Skip to content

Commit ffdc27d

Browse files
Merge branch 'master' into CECHO-917
TICKET: CECHO-917
2 parents 3b14a96 + a38c3bf commit ffdc27d

75 files changed

Lines changed: 2791 additions & 153 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CODEOWNERS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
/modules/utxo-lib/ @BitGo/btc-team
2828
/modules/utxo-ord/ @BitGo/btc-team
2929
/modules/utxo-staking/ @BitGo/btc-team
30+
/modules/utxo-descriptors/ @BitGo/btc-team @BitGo/ethalt-team
3031
/modules/babylonlabs-io-btc-staking-ts @BitGo/btc-team
3132
/modules/bitgo/test/v2/unit/coins/abstractUtxoCoin.ts @BitGo/btc-team
3233
/modules/bitgo/test/v2/unit/coins/payGoPSBTHexFixture/psbtHexProof.ts @BitGo/btc-team

modules/bitgo/src/v2/coinFactory.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ import {
4242
PolyxTokenConfig,
4343
JettonTokenConfig,
4444
CantonTokenConfig,
45+
Erc7984TokenConfig,
46+
Erc7984Coin,
4547
} from '@bitgo/statics';
4648
import {
4749
Ada,
@@ -82,6 +84,7 @@ import {
8284
EosToken,
8385
Erc20Token,
8486
Erc721Token,
87+
Erc7984Token,
8588
Etc,
8689
Eth,
8790
Ethw,
@@ -419,6 +422,13 @@ export function registerCoinConstructors(coinFactory: CoinFactory, coinMap: Coin
419422
}
420423
);
421424

425+
Erc7984Token.createTokenConstructors([
426+
...tokens.bitcoin.eth.confidentialTokens,
427+
...tokens.testnet.eth.confidentialTokens,
428+
]).forEach(({ name, coinConstructor }) => {
429+
coinFactory.register(name, coinConstructor);
430+
});
431+
422432
StellarToken.createTokenConstructors([...tokens.bitcoin.xlm.tokens, ...tokens.testnet.xlm.tokens]).forEach(
423433
({ name, coinConstructor }) => {
424434
coinFactory.register(name, coinConstructor);
@@ -960,12 +970,16 @@ export function getTokenConstructor(tokenConfig: TokenConfig): CoinConstructor |
960970

961971
switch (tokenConfig.coin) {
962972
case 'eth':
963-
case 'hteth':
964-
if (tokenConfig.type.includes('erc721')) {
973+
case 'hteth': {
974+
const staticCoin = coins.get(tokenConfig.type);
975+
if (staticCoin instanceof Erc7984Coin) {
976+
return Erc7984Token.createTokenConstructor(tokenConfig as Erc7984TokenConfig);
977+
} else if (tokenConfig.type.includes('erc721')) {
965978
return Erc721Token.createTokenConstructor(tokenConfig as EthLikeTokenConfig);
966979
} else {
967980
return Erc20Token.createTokenConstructor(tokenConfig as Erc20TokenConfig);
968981
}
982+
}
969983
case 'xlm':
970984
case 'txlm':
971985
return StellarToken.createTokenConstructor(tokenConfig as StellarTokenConfig);

modules/bitgo/src/v2/coins/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import { Doge, Tdoge } from '@bitgo/sdk-coin-doge';
3030
import { Dot, Tdot } from '@bitgo/sdk-coin-dot';
3131
import { Eos, EosToken, Teos } from '@bitgo/sdk-coin-eos';
3232
import { Etc, Tetc } from '@bitgo/sdk-coin-etc';
33-
import { Erc20Token, Erc721Token, Eth, Gteth, Hteth, Teth } from '@bitgo/sdk-coin-eth';
33+
import { Erc20Token, Erc721Token, Erc7984Token, Eth, Gteth, Hteth, Teth } from '@bitgo/sdk-coin-eth';
3434
import { EvmCoin, EthLikeErc20Token, EthLikeErc721Token } from '@bitgo/sdk-coin-evm';
3535
import { Flr, Tflr, FlrToken } from '@bitgo/sdk-coin-flr';
3636
import { Flrp } from '@bitgo/sdk-coin-flrp';
@@ -107,7 +107,7 @@ export { Doge, Tdoge };
107107
export { Dot, Tdot };
108108
export { Bcha, Tbcha };
109109
export { Eos, EosToken, Teos };
110-
export { Erc20Token, Erc721Token, Eth, Gteth, Hteth, Teth };
110+
export { Erc20Token, Erc721Token, Erc7984Token, Eth, Gteth, Hteth, Teth };
111111
export { Ethw };
112112
export { EthLikeCoin, TethLikeCoin };
113113
export { Etc, Tetc };

modules/bitgo/test/browser/browser.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ describe('Coins', () => {
1717
AdaToken: 1,
1818
Erc20Token: 1,
1919
Erc721Token: 1,
20+
Erc7984Token: 1,
2021
EthLikeCoin: 1,
2122
TethLikeCoin: 1,
2223
OfcToken: 1,

modules/bitgo/test/v2/unit/internal/tssUtils/eddsa.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
common,
1616
createSharedDataProof,
1717
Ed25519BIP32,
18+
EDDSAUtils,
1819
Eddsa,
1920
EncryptedSignerShareType,
2021
ExchangeCommitmentResponse,
@@ -1130,11 +1131,24 @@ describe('TSS Utils:', async function () {
11301131
coldWalletTssUtils.supportedTxRequestVersions().should.deepEqual(['full']);
11311132
});
11321133
it('should return full and lite for hot wallets', async function () {
1133-
const hotWallet = new Wallet(bitgo, baseCoin, { multisigType: 'tss', type: 'hot' });
1134+
const hotWallet = new Wallet(bitgo, baseCoin, {
1135+
multisigType: 'tss',
1136+
multisigTypeVersion: undefined,
1137+
type: 'hot',
1138+
});
11341139
const hotTssUtils = new TssUtils(bitgo, baseCoin, hotWallet);
11351140
const supportedTxRequestVersions = hotTssUtils.supportedTxRequestVersions();
11361141
supportedTxRequestVersions.should.deepEqual(['lite', 'full']);
11371142
});
1143+
it('should return only full for hot MPCv2 wallets', function () {
1144+
const hotMPCv2Wallet = new Wallet(bitgo, baseCoin, {
1145+
multisigType: 'tss',
1146+
multisigTypeVersion: 'MPCv2',
1147+
type: 'hot',
1148+
});
1149+
const mpcv2TssUtils = new EDDSAUtils.EddsaMPCv2Utils(bitgo, baseCoin, hotMPCv2Wallet);
1150+
mpcv2TssUtils.supportedTxRequestVersions().should.deepEqual(['full']);
1151+
});
11381152
it('should return empty for trading wallets', function () {
11391153
const tradingWallets = new Wallet(bitgo, baseCoin, { multisigType: 'tss', type: 'trading' });
11401154
const tradingWalletTssUtils = new TssUtils(bitgo, baseCoin, tradingWallets);

modules/bitgo/test/v2/unit/internal/tssUtils/eddsaMPCv2/signTxRequest.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,90 @@ describe('signTxRequest:', function () {
239239
.should.be.rejectedWith(/Unexpected signature share response/);
240240
});
241241

242+
it('should throw if round 2 response has wrong type', async function () {
243+
const messageBuffer = Buffer.from(signableHex, 'hex');
244+
const bitgoDsg = new EddsaMPSDsg.DSG(MPCv2PartiesEnum.BITGO);
245+
bitgoDsg.initDsg(
246+
bitgoKeyShare,
247+
messageBuffer,
248+
txRequest.transactions![0].unsignedTx.derivationPath,
249+
MPCv2PartiesEnum.USER
250+
);
251+
const bitgoMsg1 = bitgoDsg.getFirstMessage();
252+
253+
// Round 1: return a valid round1Output so the orchestration can proceed
254+
nock('https://bitgo.fakeurl')
255+
.post(
256+
`/api/v2/wallet/${txRequest.walletId}/txrequests/${txRequest.txRequestId}/transactions/0/sign`,
257+
(body) =>
258+
(JSON.parse(body.signatureShares[0].share) as EddsaMPCv2SignatureShareRound1Input).type === 'round1Input'
259+
)
260+
.reply(
261+
200,
262+
async (_uri: string, body: { signatureShares: SignatureShareRecord[]; signerGpgPublicKey: string }) => {
263+
const parsedShare = JSON.parse(body.signatureShares[0].share) as EddsaMPCv2SignatureShareRound1Input;
264+
const userMsg1Bytes = Buffer.from(parsedShare.data.msg1.message, 'base64');
265+
const userDeserializedMsg1: MPSTypes.DeserializedMessage = {
266+
from: MPCv2PartiesEnum.USER,
267+
payload: new Uint8Array(userMsg1Bytes),
268+
};
269+
// Advance bitgo session (we don't need bitgoMsg2 for this test)
270+
bitgoDsg.handleIncomingMessages([bitgoMsg1, userDeserializedMsg1]);
271+
const bitgoSignedMsg1 = await MPSComms.detachSignMpsMessage(Buffer.from(bitgoMsg1.payload), bitgoPrvKeyObj);
272+
const round1Output: EddsaMPCv2SignatureShareRound1Output = {
273+
type: 'round1Output',
274+
data: { msg1: bitgoSignedMsg1 },
275+
};
276+
return {
277+
txRequestId,
278+
transactions: [
279+
{
280+
signatureShares: [
281+
{
282+
from: SignatureShareType.BITGO,
283+
to: SignatureShareType.USER,
284+
share: JSON.stringify(round1Output),
285+
},
286+
],
287+
},
288+
],
289+
};
290+
}
291+
);
292+
293+
// Round 2: return a share with wrong type (round3Output instead of round2Output)
294+
nock('https://bitgo.fakeurl')
295+
.post(
296+
`/api/v2/wallet/${txRequest.walletId}/txrequests/${txRequest.txRequestId}/transactions/0/sign`,
297+
(body) =>
298+
(JSON.parse(body.signatureShares[0].share) as EddsaMPCv2SignatureShareRound2Input).type === 'round2Input'
299+
)
300+
.reply(200, {
301+
txRequestId,
302+
transactions: [
303+
{
304+
signatureShares: [
305+
{
306+
from: SignatureShareType.USER,
307+
to: SignatureShareType.BITGO,
308+
share: 'placeholder',
309+
},
310+
{
311+
from: SignatureShareType.BITGO,
312+
to: SignatureShareType.USER,
313+
share: JSON.stringify({ type: 'round3Output', data: {} }),
314+
},
315+
],
316+
},
317+
],
318+
});
319+
320+
const userPrvBase64 = Buffer.from(userKeyShare).toString('base64');
321+
await tssUtils
322+
.signTxRequest({ txRequest, prv: userPrvBase64, reqId, txParams })
323+
.should.be.rejectedWith(/Unexpected signature share response. Unable to parse data./);
324+
});
325+
242326
it('successfully signs a txRequest after receiving multiple 429 errors in round 2', async function () {
243327
const nockPromises = await getNockPromisesForEddsaSigning(txRequest, RequestType.tx, 3);
244328
await Promise.all(nockPromises);

modules/sdk-coin-canton/src/canton.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,8 @@ export class Canton extends BaseCoin {
114114
case TransactionType.TransferReject:
115115
case TransactionType.TransferAcknowledge:
116116
case TransactionType.TransferOfferWithdrawn:
117+
case TransactionType.CosignDelegationAccept:
118+
case TransactionType.CosignDelegationProposal:
117119
// There is no input for these type of transactions, so always return true.
118120
return true;
119121
case TransactionType.OneStepPreApproval:
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import { InvalidTransactionError, PublicKey, TransactionType } from '@bitgo/sdk-core';
2+
import { BaseCoin as CoinConfig } from '@bitgo/statics';
3+
import { CantonPrepareCommandResponse, CantonTransferAcceptRejectRequest } from './iface';
4+
import { TransactionBuilder } from './transactionBuilder';
5+
import { Transaction } from './transaction/transaction';
6+
import utils from './utils';
7+
8+
export class CosignDelegationAcceptBuilder extends TransactionBuilder {
9+
private _commandId: string;
10+
private _contractId: string;
11+
private _actAsPartyId: string;
12+
13+
constructor(_coinConfig: Readonly<CoinConfig>) {
14+
super(_coinConfig);
15+
}
16+
17+
initBuilder(tx: Transaction): void {
18+
super.initBuilder(tx);
19+
this.setTransactionType();
20+
}
21+
22+
get transactionType(): TransactionType {
23+
return TransactionType.CosignDelegationAccept;
24+
}
25+
26+
setTransactionType(): void {
27+
this.transaction.transactionType = TransactionType.CosignDelegationAccept;
28+
}
29+
30+
setTransaction(transaction: CantonPrepareCommandResponse): void {
31+
this.transaction.prepareCommand = transaction;
32+
}
33+
34+
/** @inheritDoc */
35+
addSignature(publicKey: PublicKey, signature: Buffer): void {
36+
if (!this.transaction) {
37+
throw new InvalidTransactionError('transaction is empty!');
38+
}
39+
this._signatures.push({ publicKey, signature });
40+
const pubKeyBase64 = utils.getBase64FromHex(publicKey.pub);
41+
this.transaction.signerFingerprint = utils.getAddressFromPublicKey(pubKeyBase64);
42+
this.transaction.signatures = signature.toString('base64');
43+
}
44+
45+
/**
46+
* Sets the unique command id for the cosign delegation accept
47+
* Also sets the _id of the transaction
48+
*
49+
* @param id - A uuid
50+
* @returns The current builder instance for chaining.
51+
* @throws Error if id is empty.
52+
*/
53+
commandId(id: string): this {
54+
if (!id || !id.trim()) {
55+
throw new Error('commandId must be a non-empty string');
56+
}
57+
this._commandId = id.trim();
58+
this.transaction.id = id.trim();
59+
return this;
60+
}
61+
62+
/**
63+
* Sets the contract id of the delegation proposal to accept
64+
* @param id - canton contract id
65+
* @returns The current builder instance for chaining.
66+
* @throws Error if id is empty.
67+
*/
68+
contractId(id: string): this {
69+
if (!id || !id.trim()) {
70+
throw new Error('contractId must be a non-empty string');
71+
}
72+
this._contractId = id.trim();
73+
return this;
74+
}
75+
76+
/**
77+
* Sets the party acting as the acceptor
78+
*
79+
* @param id - the actor party id (address)
80+
* @returns The current builder instance for chaining.
81+
* @throws Error if id is empty.
82+
*/
83+
actAs(id: string): this {
84+
if (!id || !id.trim()) {
85+
throw new Error('actAsPartyId must be a non-empty string');
86+
}
87+
this._actAsPartyId = id.trim();
88+
return this;
89+
}
90+
91+
/**
92+
* Builds and returns the CantonTransferAcceptRejectRequest object from the builder's internal state.
93+
*
94+
* @returns {CantonTransferAcceptRejectRequest} - A fully constructed and validated request object.
95+
* @throws {Error} If any required field is missing or fails validation.
96+
*/
97+
toRequestObject(): CantonTransferAcceptRejectRequest {
98+
this.validate();
99+
100+
return {
101+
commandId: this._commandId,
102+
contractId: this._contractId,
103+
verboseHashing: false,
104+
actAs: [this._actAsPartyId],
105+
readAs: [],
106+
};
107+
}
108+
109+
/**
110+
* Validates the internal state of the builder before building the request object.
111+
*
112+
* @private
113+
* @throws {Error} If any required field is missing or invalid.
114+
*/
115+
private validate(): void {
116+
if (!this._commandId) throw new Error('commandId is missing');
117+
if (!this._contractId) throw new Error('contractId is missing');
118+
if (!this._actAsPartyId) throw new Error('actAs partyId is missing');
119+
}
120+
}

0 commit comments

Comments
 (0)