diff --git a/modules/bitgo/test/v2/unit/wallet.ts b/modules/bitgo/test/v2/unit/wallet.ts index 91e8b6f618..0cfadb0789 100644 --- a/modules/bitgo/test/v2/unit/wallet.ts +++ b/modules/bitgo/test/v2/unit/wallet.ts @@ -1247,6 +1247,87 @@ describe('V2 Wallet:', function () { }); }); + describe('Canton tests: ', () => { + let cantonWallet: Wallet; + const cantonBitgo = TestBitGo.decorate(BitGo, { env: 'mock' }); + cantonBitgo.initializeTestVars(); + const walletData = { + id: '598f606cd8fc24710d2ebadb1d9459bb', + coinSpecific: { + baseAddress: '12205::12205b4e3537a95126d90604592344d8ad3c3ddccda4f79901954280ee19c576714d', + pendingChainInitialization: true, + lastChainIndex: { 0: 0 }, + }, + coin: 'tcanton', + keys: [ + '598f606cd8fc24710d2ebad89dce86c2', + '598f606cc8e43aef09fcb785221d9dd2', + '5935d59cf660764331bafcade1855fd7', + ], + multisigType: 'tss', + }; + + before(async function () { + cantonWallet = new Wallet(bitgo, bitgo.coin('tcanton'), walletData); + nock(bgUrl).get(`/api/v2/${cantonWallet.coin()}/key/${cantonWallet.keyIds()[0]}`).times(3).reply(200, { + id: '598f606cd8fc24710d2ebad89dce86c2', + pub: '5f8WmC2uW9SAk7LMX2r4G1Bx8MMwx8sdgpotyHGodiZo', + source: 'user', + encryptedPrv: + '{"iv":"hNK3rg82P1T94MaueXFAbA==","v":1,"iter":10000,"ks":256,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"cV4wU4EzPjs=","ct":"9VZX99Ztsb6p75Cxl2lrcXBplmssIAQ9k7ZA81vdDYG4N5dZ36BQNWVfDoelj9O31XyJ+Xri0XKIWUzl0KKLfUERplmtNoOCn5ifJcZwCrOxpHZQe3AJ700o8Wmsrk5H"}', + coinSpecific: {}, + }); + + nock(bgUrl).get(`/api/v2/${cantonWallet.coin()}/key/${cantonWallet.keyIds()[1]}`).times(2).reply(200, { + id: '598f606cc8e43aef09fcb785221d9dd2', + pub: 'G1s43JTzNZzqhUn4aNpwgcc6wb9FUsZQD5JjffG6isyd', + encryptedPrv: + '{"iv":"UFrt/QlIUR1XeQafPBaAlw==","v":1,"iter":10000,"ks":256,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"7VPBYaJXPm8=","ct":"ajFKv2y8yaIBXQ39sAbBWcnbiEEzbjS4AoQtp5cXYqjeDRxt3aCxemPm22pnkJaCijFjJrMHbkmsNhNYzHg5aHFukN+nEAVssyNwHbzlhSnm8/BVN50yAdAAtWreh8cp"}', + source: 'backup', + coinSpecific: {}, + }); + + nock(bgUrl).get(`/api/v2/${cantonWallet.coin()}/key/${cantonWallet.keyIds()[2]}`).times(2).reply(200, { + id: '5935d59cf660764331bafcade1855fd7', + pub: 'GH1LV1e9FdqGe8U2c8PMEcma3fDeh1ktcGVBrD3AuFqx', + encryptedPrv: + '{"iv":"iIuWOHIOErEDdiJn6g46mg==","v":1,"iter":10000,"ks":256,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"Rzh7RRJksj0=","ct":"rcNICUfp9FakT53l+adB6XKzS1vNTc0Qq9jAtqnxA+ScssiS4Q0l3sgG/0gDy5DaZKtXryKBDUvGsi7b/fYaFCUpAoZn/VZTOhOUN/mo7ZHb4OhOXL29YPPkiryAq9Cr"}', + source: 'bitgo', + coinSpecific: {}, + }); + }); + + after(async function () { + nock.cleanAll(); + }); + + it('Should build wallet initialization transactions correctly', async function () { + const txRequestNock = nock(bgUrl) + .post(`/api/v2/wallet/${cantonWallet.id()}/txrequests`) + .reply((url, body) => { + const bodyParams = body as any; + bodyParams.intent.intentType.should.equal('createAccount'); + bodyParams.intent.recipients.length.should.equal(0); + return [ + 200, + { + apiVersion: 'full', + transactions: [ + { + unsignedTx: { + serializedTxHex: 'fake transaction', + feeInfo: 'fake fee info', + }, + }, + ], + }, + ]; + }); + await cantonWallet.sendWalletInitialization(); + txRequestNock.isDone().should.equal(true); + }); + }); + describe('Solana tests: ', () => { let solWallet: Wallet; const passphrase = '#Bondiola1234'; diff --git a/modules/sdk-coin-canton/src/canton.ts b/modules/sdk-coin-canton/src/canton.ts index a7c88d65ec..ef691d9490 100644 --- a/modules/sdk-coin-canton/src/canton.ts +++ b/modules/sdk-coin-canton/src/canton.ts @@ -67,6 +67,11 @@ export class Canton extends BaseCoin { return multisigTypes.tss; } + /** inherited doc */ + requiresWalletInitializationTransaction(): boolean { + return true; + } + getMPCAlgorithm(): MPCAlgorithm { return 'eddsa'; } diff --git a/modules/sdk-coin-canton/src/lib/walletInitialization/walletInitTransaction.ts b/modules/sdk-coin-canton/src/lib/walletInitialization/walletInitTransaction.ts index 6e536aa0b6..1b4ad90ad6 100644 --- a/modules/sdk-coin-canton/src/lib/walletInitialization/walletInitTransaction.ts +++ b/modules/sdk-coin-canton/src/lib/walletInitialization/walletInitTransaction.ts @@ -79,7 +79,7 @@ export class WalletInitTransaction extends BaseTransaction { if (!this._preparedParty) { throw new InvalidTransactionError('Empty transaction data'); } - return Buffer.from(this._preparedParty.multiHash); + return Buffer.from(this._preparedParty.multiHash, 'base64'); } fromRawTransaction(rawTx: string): void { diff --git a/modules/sdk-core/src/bitgo/baseCoin/baseCoin.ts b/modules/sdk-core/src/bitgo/baseCoin/baseCoin.ts index 45332efb46..368c71de65 100644 --- a/modules/sdk-core/src/bitgo/baseCoin/baseCoin.ts +++ b/modules/sdk-core/src/bitgo/baseCoin/baseCoin.ts @@ -381,6 +381,14 @@ export abstract class BaseCoin implements IBaseCoin { return false; } + /** + * Check whether a coin requires wallet initialization + * @returns {boolean} + */ + requiresWalletInitializationTransaction(): boolean { + return false; + } + /** * Check whether a coin supports signing of Typed data * @returns {boolean} diff --git a/modules/sdk-core/src/bitgo/baseCoin/iBaseCoin.ts b/modules/sdk-core/src/bitgo/baseCoin/iBaseCoin.ts index e22677c966..0accf03565 100644 --- a/modules/sdk-core/src/bitgo/baseCoin/iBaseCoin.ts +++ b/modules/sdk-core/src/bitgo/baseCoin/iBaseCoin.ts @@ -604,4 +604,5 @@ export interface IBaseCoin { * @param {string} params.multiSigType - The type of multisig (e.g. 'onchain' or 'tss') */ assertIsValidKey({ publicKey, encryptedPrv, walletPassphrase, multiSigType }: AuditKeyParams): void; + requiresWalletInitializationTransaction(): boolean; } diff --git a/modules/sdk-core/src/bitgo/wallet/iWallet.ts b/modules/sdk-core/src/bitgo/wallet/iWallet.ts index 735d4408a6..2c1e6fee7c 100644 --- a/modules/sdk-core/src/bitgo/wallet/iWallet.ts +++ b/modules/sdk-core/src/bitgo/wallet/iWallet.ts @@ -893,6 +893,11 @@ export type SendNFTResult = { pendingApproval: PendingApprovalData; }; +export type WalletInitResult = { + success: PrebuildTransactionResult[]; + failure: Error[]; +}; + export interface IWallet { bitgo: BitGoBase; baseCoin: IBaseCoin; @@ -985,6 +990,7 @@ export interface IWallet { buildTokenEnablements(params?: BuildTokenEnablementOptions): Promise; sendTokenEnablement(params?: PrebuildAndSignTransactionOptions): Promise; sendTokenEnablements(params?: BuildTokenEnablementOptions): Promise; + sendWalletInitialization(params?: PrebuildTransactionOptions): Promise; signMessage(params: WalletSignMessageOptions): Promise; buildSignMessageRequest(params: WalletSignMessageOptions): Promise; signTypedData(params: WalletSignTypedDataOptions): Promise; diff --git a/modules/sdk-core/src/bitgo/wallet/wallet.ts b/modules/sdk-core/src/bitgo/wallet/wallet.ts index bec6a9c890..c588c067f5 100644 --- a/modules/sdk-core/src/bitgo/wallet/wallet.ts +++ b/modules/sdk-core/src/bitgo/wallet/wallet.ts @@ -123,6 +123,7 @@ import { WalletSignTypedDataOptions, WalletType, BuildTokenApprovalResponse, + WalletInitResult, } from './iWallet'; const debug = require('debug')('bitgo:v2:wallet'); @@ -3319,6 +3320,48 @@ export class Wallet implements IWallet { }; } + /** + * The chain canton, requires the wallet to be initialized by sending out a transaction + * to be onboarded onto the validator. + * Builds, Signs and sends a transaction that initializes the canton wallet + * @param params + */ + public async sendWalletInitialization(params: PrebuildAndSignTransactionOptions = {}): Promise { + if (!this.baseCoin.requiresWalletInitializationTransaction()) { + throw new Error(`Wallet initialization is not required for ${this.baseCoin.getFullName()}`); + } + if (this._wallet.multisigType !== 'tss') { + throw new Error('Wallet initialization transaction is only supported for TSS wallets'); + } + if (params.reqId) { + this.bitgo.setRequestTracer(params.reqId); + } + const buildParams: PrebuildTransactionOptions = _.pick(params, this.prebuildWhitelistedParams()); + if (!buildParams.type) { + buildParams.type = 'createAccount'; + } + const prebuildTx = await this.prebuildTransaction(buildParams); + const unsignedBuildWithOptions: PrebuildAndSignTransactionOptions = { + ...params, + prebuildTx, + }; + if (typeof params.prebuildTx === 'string' || params.prebuildTx?.buildParams?.type !== 'createAccount') { + throw new Error('Invalid build of wallet init'); + } + const successfulTxs: PrebuildTransactionResult[] = []; + const failedTxs = new Array(); + try { + const sendTx = await this.sendManyTxRequests(unsignedBuildWithOptions); + successfulTxs.push(sendTx); + } catch (e) { + failedTxs.push(e); + } + return { + success: successfulTxs, + failure: failedTxs, + }; + } + /* MARK: TSS Helpers */ /** @@ -3439,6 +3482,16 @@ export class Wallet implements IWallet { params.preview ); break; + case 'createAccount': + txRequest = await this.tssUtils!.prebuildTxWithIntent( + { + reqId, + intentType: 'createAccount', + }, + apiVersion, + params.preview + ); + break; case 'customTx': txRequest = await this.tssUtils!.prebuildTxWithIntent( {