From 020b722fa2757cc77c3e8ca1555ca7a35ada233f Mon Sep 17 00:00:00 2001 From: Shobhit B Date: Fri, 5 Jun 2026 17:05:04 +0530 Subject: [PATCH 01/34] feat: exclude singapore trust for ab1 TICKET: CECHO-986 --- modules/statics/src/coins/solTokens.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/statics/src/coins/solTokens.ts b/modules/statics/src/coins/solTokens.ts index efb9f5e507..31f1f5f03b 100644 --- a/modules/statics/src/coins/solTokens.ts +++ b/modules/statics/src/coins/solTokens.ts @@ -4011,7 +4011,7 @@ export const solTokens = [ '13UnWveBycSEeYJFgvsmE2qHXP5ERienrhGZ5wUjWM8L', '13UnWveBycSEeYJFgvsmE2qHXP5ERienrhGZ5wUjWM8L', UnderlyingAsset['sol:ab1'], - SOL_TOKEN_FEATURES, + SOL_TOKEN_FEATURES_EXCLUDE_SINGAPORE, ProgramID.Token2022ProgramId ), ]; From 74665341dfe611d3475f8777b39462d0076692ea Mon Sep 17 00:00:00 2001 From: Daniel Peng Date: Fri, 5 Jun 2026 11:54:46 -0400 Subject: [PATCH 02/34] feat: external signer callback for multisig akm wallet gen Ticket: WCN-685 --- modules/sdk-core/src/bitgo/wallet/iWallets.ts | 22 ++ modules/sdk-core/src/bitgo/wallet/wallets.ts | 163 ++++++++++++- .../bitgo/wallet/walletsExternalSigner.ts | 221 ++++++++++++++++++ 3 files changed, 405 insertions(+), 1 deletion(-) create mode 100644 modules/sdk-core/test/unit/bitgo/wallet/walletsExternalSigner.ts diff --git a/modules/sdk-core/src/bitgo/wallet/iWallets.ts b/modules/sdk-core/src/bitgo/wallet/iWallets.ts index ac82fc56bd..5aecb03edc 100644 --- a/modules/sdk-core/src/bitgo/wallet/iWallets.ts +++ b/modules/sdk-core/src/bitgo/wallet/iWallets.ts @@ -63,6 +63,19 @@ export interface GenerateSMCMpcWalletOptions extends GenerateBaseMpcWalletOption coldDerivationSeed?: string; } +export interface CreateKeychainCallbackParams { + source: 'user' | 'backup'; + coin: string; +} + +export interface CreateKeychainCallbackResult { + pub: string; + type: string; + source: string; +} + +export type CreateKeychainCallback = (params: CreateKeychainCallbackParams) => Promise; + export interface GenerateWalletOptions { label?: string; passphrase?: string; @@ -95,6 +108,14 @@ export interface GenerateWalletOptions { /** Optional WebAuthn PRF-based encryption info. When provided, the user private key is additionally encrypted with the PRF-derived passphrase so the server can store a WebAuthn-protected copy. */ webauthnInfo?: WebauthnKeyEncryptionInfo; encryptionVersion?: EncryptionVersion; + /** Delegates user/backup key creation to an external signer (onchain multisig only). */ + createKeychainCallback?: CreateKeychainCallback; +} + +export interface GenerateWalletWithExternalSignerOptions + extends Omit { + label: string; + createKeychainCallback: CreateKeychainCallback; } export const GenerateLightningWalletOptionsCodec = t.intersection( @@ -281,6 +302,7 @@ export interface IWallets { generateWallet( params?: GenerateWalletOptions ): Promise; + generateWalletWithExternalSigner(params: GenerateWalletWithExternalSignerOptions): Promise; listShares(params?: Record): Promise; getShare(params?: { walletShareId?: string }): Promise; updateShare(params?: UpdateShareOptions): Promise; diff --git a/modules/sdk-core/src/bitgo/wallet/wallets.ts b/modules/sdk-core/src/bitgo/wallet/wallets.ts index 2a6916707b..9d1c12c899 100644 --- a/modules/sdk-core/src/bitgo/wallet/wallets.ts +++ b/modules/sdk-core/src/bitgo/wallet/wallets.ts @@ -12,7 +12,7 @@ import * as common from '../../common'; import { IBaseCoin, KeychainsTriplet, SupplementGenerateWalletOptions } from '../baseCoin'; import { BitGoBase } from '../bitgoBase'; import { getSharedSecret } from '../ecdh'; -import { AddKeychainOptions, Keychain, KeyIndices } from '../keychain'; +import { AddKeychainOptions, Keychain, KeyIndices, KeyType } from '../keychain'; import { decodeOrElse, promiseProps, RequestTracer } from '../utils'; import { AcceptShareOptions, @@ -31,6 +31,7 @@ import { GenerateMpcWalletOptions, GenerateSMCMpcWalletOptions, GenerateWalletOptions, + GenerateWalletWithExternalSignerOptions, GetWalletByAddressOptions, GetWalletOptions, GoAccountWalletWithUserKeychain, @@ -360,6 +361,10 @@ export class Wallets implements IWallets { throw new Error('missing required string parameter label'); } + if (params.createKeychainCallback) { + return this.generateWalletWithExternalSigner(params as GenerateWalletWithExternalSignerOptions); + } + const { type = 'hot', label, passphrase, enterprise, isDistributedCustody, evmKeyRingReferenceWalletId } = params; const isTss = params.multisigType === 'tss' && this.baseCoin.supportsTss(); const canEncrypt = !!passphrase && typeof passphrase === 'string'; @@ -718,6 +723,162 @@ export class Wallets implements IWallets { } } + /** + * Generate an onchain multisig wallet using an external signer for user and backup key creation. + * 1. Calls createKeychainCallback for user and backup keys + * 2. Uploads keychains via keychains().add() + * 3. Creates the BitGo key on the service + * 4. Creates the wallet on BitGo with the 3 public keys + * @param params + */ + async generateWalletWithExternalSigner( + params: GenerateWalletWithExternalSignerOptions + ): Promise { + if (!_.isFunction(params.createKeychainCallback)) { + throw new Error('missing required function parameter createKeychainCallback'); + } + + const multisigType = params.multisigType ?? this.baseCoin.getDefaultMultisigType(); + if (multisigType !== 'onchain') { + throw new Error('external signer wallet generation is only supported for onchain multisig wallets'); + } + + const conflictingParams = ['passphrase', 'userKey', 'backupXpub', 'backupXpubProvider'] as const; + for (const key of conflictingParams) { + if (!_.isUndefined(params[key])) { + throw new Error(`createKeychainCallback cannot be used with ${key}`); + } + } + + const { label, createKeychainCallback, type = 'hot', enterprise, isDistributedCustody } = params; + + if (type === 'custodial') { + throw new Error('external signer wallet generation is not supported for custodial onchain wallets'); + } + + if (!_.isUndefined(params.webauthnInfo)) { + throw new Error('webauthnInfo is not supported for external signer wallet generation'); + } + + if (!_.isUndefined(params.passcodeEncryptionCode)) { + throw new Error('passcodeEncryptionCode is not supported for external signer wallet generation'); + } + + if (isDistributedCustody) { + if (!enterprise) { + throw new Error('must provide enterprise when creating distributed custody wallet'); + } + if (type !== 'cold') { + throw new Error('distributed custody wallets must be type: cold'); + } + } + + if (params.gasPrice && params.eip1559) { + throw new Error('can not use both eip1559 and gasPrice values'); + } + + const walletParams: SupplementGenerateWalletOptions = { + label, + m: 2, + n: 3, + keys: [], + type, + }; + + if (!_.isUndefined(enterprise)) { + if (!_.isString(enterprise)) { + throw new Error('invalid enterprise argument, expecting string'); + } + walletParams.enterprise = enterprise; + } + + if (!_.isUndefined(params.disableTransactionNotifications)) { + if (!_.isBoolean(params.disableTransactionNotifications)) { + throw new Error('invalid disableTransactionNotifications argument, expecting boolean'); + } + walletParams.disableTransactionNotifications = params.disableTransactionNotifications; + } + + if (!_.isUndefined(params.gasPrice)) { + const gasPriceBN = new BigNumber(params.gasPrice); + if (gasPriceBN.isNaN()) { + throw new Error('invalid gas price argument, expecting number or number as string'); + } + walletParams.gasPrice = gasPriceBN.toString(); + } + + if (!_.isUndefined(params.eip1559) && !_.isEmpty(params.eip1559)) { + const maxFeePerGasBN = new BigNumber(params.eip1559.maxFeePerGas); + if (maxFeePerGasBN.isNaN()) { + throw new Error('invalid max fee argument, expecting number or number as string'); + } + const maxPriorityFeePerGasBN = new BigNumber(params.eip1559.maxPriorityFeePerGas); + if (maxPriorityFeePerGasBN.isNaN()) { + throw new Error('invalid priority fee argument, expecting number or number as string'); + } + walletParams.eip1559 = { + maxFeePerGas: maxFeePerGasBN.toString(), + maxPriorityFeePerGas: maxPriorityFeePerGasBN.toString(), + }; + } + + if (!_.isUndefined(params.walletVersion)) { + if (!_.isNumber(params.walletVersion)) { + throw new Error('invalid walletVersion provided, expecting number'); + } + walletParams.walletVersion = params.walletVersion; + } + + const reqId = new RequestTracer(); + const coin = this.baseCoin.getChain(); + + const createAndUploadKeychain = async (source: 'user' | 'backup'): Promise => { + const keychainFromCallback = await createKeychainCallback({ source, coin }); + if (keychainFromCallback.source !== source) { + throw new Error(`createKeychainCallback returned source ${keychainFromCallback.source}, expected ${source}`); + } + return this.baseCoin.keychains().add({ + pub: keychainFromCallback.pub, + keyType: keychainFromCallback.type as KeyType, + source: keychainFromCallback.source, + reqId, + }); + }; + + const { userKeychain, backupKeychain, bitgoKeychain }: KeychainsTriplet = await promiseProps({ + userKeychain: createAndUploadKeychain('user'), + backupKeychain: createAndUploadKeychain('backup'), + bitgoKeychain: this.baseCoin + .keychains() + .createBitGo({ enterprise, reqId, isDistributedCustody: params.isDistributedCustody }), + }); + + walletParams.keys = [userKeychain.id, backupKeychain.id, bitgoKeychain.id]; + + const keychains = { + userKeychain, + backupKeychain, + bitgoKeychain, + }; + + if (_.includes(['xrp', 'xlm', 'cspr'], this.baseCoin.getFamily()) && !_.isUndefined(params.rootPrivateKey)) { + walletParams.rootPrivateKey = params.rootPrivateKey; + } + + const finalWalletParams = await this.baseCoin.supplementGenerateWallet(walletParams, keychains); + + this.bitgo.setRequestTracer(reqId); + const newWallet = await this.bitgo.post(this.baseCoin.url('/wallet/add')).send(finalWalletParams).result(); + + return { + wallet: new Wallet(this.bitgo, this.baseCoin, newWallet), + userKeychain, + backupKeychain, + bitgoKeychain, + responseType: 'WalletWithKeychains', + }; + } + /** * List the user's wallet shares * @param params diff --git a/modules/sdk-core/test/unit/bitgo/wallet/walletsExternalSigner.ts b/modules/sdk-core/test/unit/bitgo/wallet/walletsExternalSigner.ts new file mode 100644 index 0000000000..cae275df70 --- /dev/null +++ b/modules/sdk-core/test/unit/bitgo/wallet/walletsExternalSigner.ts @@ -0,0 +1,221 @@ +import * as assert from 'assert'; +import * as sinon from 'sinon'; +import 'should'; + +import { Wallets } from '../../../../src/bitgo/wallet/wallets'; +import { CreateKeychainCallback } from '../../../../src/bitgo/wallet/iWallets'; + +describe('Wallets - external signer onchain wallet generation', function () { + let wallets: Wallets; + let mockBitGo: any; + let mockBaseCoin: any; + let mockKeychains: any; + let createKeychainCallback: sinon.SinonStub, ReturnType>; + let sendStub: sinon.SinonStub; + + const userPub = + 'xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8'; + const backupPub = + 'xpub661MyMwAqRbcGczjuMoRm6dXaLDEhW1u34gKenbeYqAix21mdUKJyuyu5F1rzYGVxyL6tmgBUAEPrEz92mBXjByMRiJdba9wpnN37RLLAXa'; + const bitgoPub = + 'xpub661MyMwAqRbcEYS8w7XLSVeEsBXy79zSzH1J8vCdxAZningWLdN3zgtU6LBpB85b3D2yc8sfvZU521AAwdZafEz7mnzBBsz4wKY5fTtTQBm'; + + beforeEach(function () { + createKeychainCallback = sinon.stub(); + createKeychainCallback.withArgs({ source: 'user', coin: 'tbtc' }).resolves({ + pub: userPub, + type: 'tbtc', + source: 'user', + }); + createKeychainCallback.withArgs({ source: 'backup', coin: 'tbtc' }).resolves({ + pub: backupPub, + type: 'tbtc', + source: 'backup', + }); + + mockKeychains = { + add: sinon.stub().callsFake(async (params: { pub: string; source: string }) => ({ + id: `${params.source}-key-id`, + pub: params.pub, + source: params.source, + })), + createBitGo: sinon.stub().resolves({ id: 'bitgo-key-id', pub: bitgoPub }), + }; + + const mockWalletData = { id: 'wallet-id', keys: ['user-key-id', 'backup-key-id', 'bitgo-key-id'] }; + + sendStub = sinon.stub().returns({ + result: sinon.stub().resolves(mockWalletData), + }); + mockBitGo = { + post: sinon.stub().returns({ send: sendStub }), + setRequestTracer: sinon.stub(), + }; + + mockBaseCoin = { + isEVM: sinon.stub().returns(false), + supportsTss: sinon.stub().returns(true), + getFamily: sinon.stub().returns('btc'), + getChain: sinon.stub().returns('tbtc'), + getDefaultMultisigType: sinon.stub().returns('onchain'), + keychains: sinon.stub().returns(mockKeychains), + url: sinon.stub().returns('/api/v2/tbtc/wallet/add'), + getConfig: sinon.stub().returns({ features: [] }), + supplementGenerateWallet: sinon.stub().callsFake((walletParams: unknown) => Promise.resolve(walletParams)), + }; + + wallets = new Wallets(mockBitGo, mockBaseCoin); + }); + + afterEach(function () { + sinon.restore(); + }); + + describe('generateWalletWithExternalSigner', function () { + it('should create user and backup keys via callback and create wallet', async function () { + const result = await wallets.generateWalletWithExternalSigner({ + label: 'External Signer Wallet', + enterprise: 'enterprise-id', + createKeychainCallback, + }); + + assert.strictEqual(createKeychainCallback.callCount, 2); + assert.strictEqual(mockKeychains.add.callCount, 2); + assert.strictEqual(mockKeychains.createBitGo.calledOnce, true); + assert.strictEqual(mockBitGo.post.calledOnce, true); + + const addUserParams = mockKeychains.add.getCall(0).args[0]; + addUserParams.should.have.property('pub', userPub); + addUserParams.should.have.property('keyType', 'tbtc'); + addUserParams.should.have.property('source', 'user'); + + const walletBody = sendStub.firstCall.args[0]; + walletBody.keys.should.deepEqual(['user-key-id', 'backup-key-id', 'bitgo-key-id']); + walletBody.label.should.equal('External Signer Wallet'); + walletBody.enterprise.should.equal('enterprise-id'); + + result.responseType.should.equal('WalletWithKeychains'); + assert.strictEqual(result.userKeychain.pub, userPub); + assert.strictEqual(result.backupKeychain.pub, backupPub); + assert.strictEqual(result.bitgoKeychain.pub, bitgoPub); + }); + + it('should reject when callback source does not match requested source', async function () { + createKeychainCallback.withArgs({ source: 'user', coin: 'tbtc' }).resolves({ + pub: userPub, + type: 'tbtc', + source: 'backup', + }); + + await wallets + .generateWalletWithExternalSigner({ + label: 'External Signer Wallet', + createKeychainCallback, + }) + .should.be.rejectedWith('createKeychainCallback returned source backup, expected user'); + }); + + it('should reject TSS multisig type', async function () { + await wallets + .generateWalletWithExternalSigner({ + label: 'TSS Wallet', + multisigType: 'tss', + createKeychainCallback, + }) + .should.be.rejectedWith('external signer wallet generation is only supported for onchain multisig wallets'); + }); + + it('should reject custodial wallet type', async function () { + await wallets + .generateWalletWithExternalSigner({ + label: 'Custodial Wallet', + type: 'custodial', + createKeychainCallback, + }) + .should.be.rejectedWith('external signer wallet generation is not supported for custodial onchain wallets'); + }); + + it('should reject passcodeEncryptionCode', async function () { + await wallets + .generateWalletWithExternalSigner({ + label: 'Wallet', + passcodeEncryptionCode: 'some-code', + createKeychainCallback, + }) + .should.be.rejectedWith('passcodeEncryptionCode is not supported for external signer wallet generation'); + }); + + it('should reject webauthnInfo', async function () { + await wallets + .generateWalletWithExternalSigner({ + label: 'Wallet', + webauthnInfo: { otpDeviceId: 'dev-id', prfSalt: 'salt', passphrase: 'pass' } as any, + createKeychainCallback, + }) + .should.be.rejectedWith('webauthnInfo is not supported for external signer wallet generation'); + }); + + it('should reject isDistributedCustody without enterprise', async function () { + await wallets + .generateWalletWithExternalSigner({ + label: 'DC Wallet', + type: 'cold', + isDistributedCustody: true, + createKeychainCallback, + }) + .should.be.rejectedWith('must provide enterprise when creating distributed custody wallet'); + }); + + it('should reject isDistributedCustody with non-cold type', async function () { + await wallets + .generateWalletWithExternalSigner({ + label: 'DC Wallet', + type: 'hot', + isDistributedCustody: true, + enterprise: 'enterprise-id', + createKeychainCallback, + }) + .should.be.rejectedWith('distributed custody wallets must be type: cold'); + }); + }); + + describe('generateWallet with createKeychainCallback', function () { + it('should delegate to generateWalletWithExternalSigner', async function () { + const generateWalletWithExternalSignerStub = sinon.stub(wallets, 'generateWalletWithExternalSigner').resolves({ + responseType: 'WalletWithKeychains', + wallet: {} as any, + userKeychain: { id: 'user-key-id', pub: userPub, type: 'independent' } as any, + backupKeychain: { id: 'backup-key-id', pub: backupPub, type: 'independent' } as any, + bitgoKeychain: { id: 'bitgo-key-id', pub: bitgoPub, type: 'independent' } as any, + }); + + await wallets.generateWallet({ + label: 'Delegated Wallet', + createKeychainCallback, + }); + + assert.strictEqual(generateWalletWithExternalSignerStub.calledOnce, true); + generateWalletWithExternalSignerStub.firstCall.args[0].label.should.equal('Delegated Wallet'); + }); + + it('should reject when createKeychainCallback is combined with passphrase', async function () { + await wallets + .generateWallet({ + label: 'Invalid Wallet', + passphrase: 'secret', + createKeychainCallback, + }) + .should.be.rejectedWith('createKeychainCallback cannot be used with passphrase'); + }); + + it('should reject when createKeychainCallback is combined with userKey', async function () { + await wallets + .generateWallet({ + label: 'Invalid Wallet', + userKey: 'xpub...', + createKeychainCallback, + }) + .should.be.rejectedWith('createKeychainCallback cannot be used with userKey'); + }); + }); +}); From 103baf9de7eef7cfa4165fe95eb4572df04b27c9 Mon Sep 17 00:00:00 2001 From: Paras Garg Date: Tue, 9 Jun 2026 16:25:25 +0530 Subject: [PATCH 03/34] feat(statics): onboard Robinhood EVM tokens - Adds 76 new tokens for the Robinhood EVM chain, plus their OFC equivalents. - Consolidate all existing hoodeth/thoodeth tokens into dedicated files. Ticket: CECHO-1213 --- modules/statics/src/allCoinsAndTokens.ts | 269 +-- modules/statics/src/base.ts | 76 + modules/statics/src/coins.ts | 2 + modules/statics/src/coins/hoodethTokens.ts | 1034 ++++++++++++ modules/statics/src/coins/ofcErc20Coins.ts | 380 ----- modules/statics/src/coins/ofcHoodethTokens.ts | 1448 +++++++++++++++++ 6 files changed, 2562 insertions(+), 647 deletions(-) create mode 100644 modules/statics/src/coins/hoodethTokens.ts create mode 100644 modules/statics/src/coins/ofcHoodethTokens.ts diff --git a/modules/statics/src/allCoinsAndTokens.ts b/modules/statics/src/allCoinsAndTokens.ts index ffd1f716a1..9e4838bb36 100644 --- a/modules/statics/src/allCoinsAndTokens.ts +++ b/modules/statics/src/allCoinsAndTokens.ts @@ -76,6 +76,7 @@ import { jettonTokens } from './coins/jettonTokens'; import { erc7984Tokens } from './coins/erc7984Tokens'; import { polyxTokens } from './coins/polyxTokens'; import { cantonTokens } from './coins/cantonTokens'; +import { hoodethTokens } from './coins/hoodethTokens'; import { flrp } from './flrp'; import { hypeEvm } from './hypeevm'; import { kaspa } from './kaspa'; @@ -171,6 +172,7 @@ export const allCoinsAndTokens = [ ...erc7984Tokens, ...polyxTokens, ...cantonTokens, + ...hoodethTokens, avaxp( '5436386e-9e4d-4d82-92df-59d9720d1738', 'avaxp', @@ -3121,24 +3123,6 @@ export const allCoinsAndTokens = [ '', 'THoodETH' ), - erc20Token( - '3493d608-fd3e-45dc-926d-783d54a8fe4d', - 'thoodeth:amzn', - 'Amazon', - 18, - '0x5884ad2f920c162cfbbacc88c9c51aa75ec09e02', - UnderlyingAsset['thoodeth:amzn'], - Networks.test.hoodeth - ), - erc20Token( - '8ede8dbd-1fa6-4669-be6d-6b19b3c98766', - 'thoodeth:tsla', - 'Tesla', - 18, - '0xc9f9c86933092bbbfff3ccb4b105a4a94bf3bd4e', - UnderlyingAsset['thoodeth:tsla'], - Networks.test.hoodeth - ), account( '1b17bbf4-02fc-492d-9071-6d7f47395f7a', 'hoodeth', @@ -3162,255 +3146,6 @@ export const allCoinsAndTokens = [ '', 'HoodETH' ), - erc20Token( - '71bfcfbb-0e1d-4712-9836-8e7c481b9d87', - 'hoodeth:tsla', - 'Tesla', - 18, - '0x322f0929c4625ed5bad873c95208d54e1c003b2d', - UnderlyingAsset['hoodeth:tsla'], - Networks.main.hoodeth - ), - erc20Token( - 'ca3ac76e-e88f-4f39-8571-006cd9b97389', - 'hoodeth:usdg', - 'USDG', - 18, - '0x5fc5360d0400a0fd4f2af552add042d716f1d168', - UnderlyingAsset['hoodeth:usdg'], - Networks.main.hoodeth - ), - // Robinhood Chain tokens - CECHO-1183 (gated for BitGo Singapore Trust) - erc20Token( - 'eada9a10-3d17-465f-978b-5a183e73f4c6', - 'hoodeth:nvda', - 'NVIDIA Corporation', - 18, - '0xd0601ce157db5bdc3162bbac2a2c8af5320d9eec', - UnderlyingAsset['hoodeth:nvda'], - Networks.main.hoodeth, - EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE - ), - erc20Token( - '13ebc2ae-5bee-4433-b5f1-94d823c0db20', - 'hoodeth:mu', - 'Micron Technology', - 18, - '0xff080c8ce2e5feadaca0da81314ae59d232d4afd', - UnderlyingAsset['hoodeth:mu'], - Networks.main.hoodeth, - EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE - ), - erc20Token( - 'b3a25405-760f-4578-bbe8-14ddfbe3aacd', - 'hoodeth:sndk', - 'SanDisk', - 18, - '0xb90a19ff0af67f7779aff50a882a9cff42446400', - UnderlyingAsset['hoodeth:sndk'], - Networks.main.hoodeth, - EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE - ), - erc20Token( - '2597a273-8103-4e42-89cd-da241c231bc0', - 'hoodeth:amd', - 'Advanced Micro Devices, Inc.', - 18, - '0x86923f96303d656e4aa86d9d42d1e57ad2023fdc', - UnderlyingAsset['hoodeth:amd'], - Networks.main.hoodeth, - EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE - ), - erc20Token( - 'e3613375-49ff-4279-9c57-e5fb9728a931', - 'hoodeth:spy', - 'SPDR S&P 500 ETF Trust', - 18, - '0x117cc2133c37b721f49de2a7a74833232b3b4c0c', - UnderlyingAsset['hoodeth:spy'], - Networks.main.hoodeth, - EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE - ), - erc20Token( - '49c855b1-0d85-4f79-9756-7ecf28440225', - 'hoodeth:msft', - 'Microsoft Corporation', - 18, - '0xe93237c50d904957cf27e7b1133b510c669c2e74', - UnderlyingAsset['hoodeth:msft'], - Networks.main.hoodeth, - EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE - ), - erc20Token( - 'f4e2c9a2-130f-4080-ba53-d43d5494ee2b', - 'hoodeth:pltr', - 'Palantir Technologies Inc.', - 18, - '0x894e1ec2d74ffe5aef8dc8a9e84686accb964f2a', - UnderlyingAsset['hoodeth:pltr'], - Networks.main.hoodeth, - EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE - ), - erc20Token( - '8aa38d75-8f24-4fe4-8642-072e3e0529bd', - 'hoodeth:intc', - 'Intel Corporation', - 18, - '0xc72b96e0e48ecd4dc75e1e45396e26300bc39681', - UnderlyingAsset['hoodeth:intc'], - Networks.main.hoodeth, - EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE - ), - erc20Token( - '92f0c908-efa9-490a-a344-15511d54ce57', - 'hoodeth:qqq', - 'Invesco QQQ Trust', - 18, - '0xd5f3879160bc7c32ebb4dc785f8a4f505888de68', - UnderlyingAsset['hoodeth:qqq'], - Networks.main.hoodeth, - EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE - ), - erc20Token( - '326f35b5-d998-4851-81b3-fcb5c0ccf0ec', - 'hoodeth:slv', - 'iShares Silver Trust', - 18, - '0x411efb0e7f985935daec3d4c3ebaea0d0ad7d89f', - UnderlyingAsset['hoodeth:slv'], - Networks.main.hoodeth, - EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE - ), - erc20Token( - 'c7491c49-a509-4791-aa18-2ab12130c518', - 'hoodeth:crcl', - 'Circle Internet Group Inc', - 18, - '0xdf0992e440dd0be65bd8439b609d6d4366bf1cb5', - UnderlyingAsset['hoodeth:crcl'], - Networks.main.hoodeth, - EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE - ), - erc20Token( - 'df3845bb-bab1-49d4-a040-c81a768d244a', - 'hoodeth:meta', - 'Meta Platforms, Inc.', - 18, - '0xc0d6457c16cc70d6790dd43521c899c87ce02f35', - UnderlyingAsset['hoodeth:meta'], - Networks.main.hoodeth, - EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE - ), - erc20Token( - 'da8769ad-98d5-4349-bea9-8fee3be83c9a', - 'hoodeth:aapl', - 'Apple Inc.', - 18, - '0xaf3d76f1834a1d425780943c99ea8a608f8a93f9', - UnderlyingAsset['hoodeth:aapl'], - Networks.main.hoodeth, - EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE - ), - erc20Token( - '31baa733-4e2b-4fc6-82ea-2653cbd083ff', - 'hoodeth:googl', - 'Alphabet Inc.', - 18, - '0x2e0847e8910a9732eb3fb1bb4b70a580adad4fe3', - UnderlyingAsset['hoodeth:googl'], - Networks.main.hoodeth, - EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE - ), - erc20Token( - 'f6f0a6f8-fea8-4306-baaf-c88a96b071de', - 'hoodeth:uso', - 'United States Oil Fund, LP', - 18, - '0xa30fa36db767ad9ed3f7a60fc79526fb4d56d344', - UnderlyingAsset['hoodeth:uso'], - Networks.main.hoodeth, - EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE - ), - erc20Token( - '6852e072-48ec-41e7-b0ad-c8594c560f5c', - 'hoodeth:amzn', - 'Amazon.com, Inc.', - 18, - '0x12f190a9f9d7d37a250758b26824b97ce941bf54', - UnderlyingAsset['hoodeth:amzn'], - Networks.main.hoodeth, - EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE - ), - erc20Token( - '9c12894d-62c6-4e9c-bea7-9d909a23c496', - 'hoodeth:crwv', - 'CoreWeave', - 18, - '0x5f10a1c971b69e47e059e1dc91901b59b3fb49c3', - UnderlyingAsset['hoodeth:crwv'], - Networks.main.hoodeth, - EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE - ), - erc20Token( - '0519b754-a119-4431-9d00-490522e2a4d1', - 'hoodeth:orcl', - 'Oracle Corporation', - 18, - '0xb0992820e760d836549ba69bc7598b4af75dee03', - UnderlyingAsset['hoodeth:orcl'], - Networks.main.hoodeth, - EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE - ), - erc20Token( - 'be7d01f7-12e9-4e19-91be-56d894c9fa9c', - 'hoodeth:sgov', - 'iShares 0-3 Month Treasury Bond ETF', - 18, - '0x92fd66527192e3e61d4ddd13322aa222de86f9b5', - UnderlyingAsset['hoodeth:sgov'], - Networks.main.hoodeth, - EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE - ), - erc20Token( - 'ffb9fa51-e51b-40f0-a2c9-3f735bab7d7c', - 'hoodeth:be', - 'Bloom Energy Corporation', - 18, - '0x822cc93ffd030293e9842c30bbd678f530701867', - UnderlyingAsset['hoodeth:be'], - Networks.main.hoodeth, - EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE - ), - erc20Token( - '7d0446f2-929f-4bea-b9b7-cb9f40dc0786', - 'hoodeth:usar', - 'USA Rare Earth Inc.', - 18, - '0xd917b029c761d264c6a312bbbcda868658ef86a6', - UnderlyingAsset['hoodeth:usar'], - Networks.main.hoodeth, - EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE - ), - erc20Token( - '7e955555-9504-4173-b9a6-2799b88e38f0', - 'hoodeth:dram', - 'Dataram Corporation', - 18, - '0x33c18e2cc8ae9ae486e785090d86b2ce632ff994', - UnderlyingAsset['hoodeth:dram'], - Networks.main.hoodeth, - EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE - ), - erc20Token( - 'ea09a0ff-059b-4527-8e7b-75dfc750218b', - 'hoodeth:week', - 'Weekly T-Bill ETF', - 18, - '0xc93a8c440cea26d7445df01729f193b27965099f', - UnderlyingAsset['hoodeth:week'], - Networks.main.hoodeth, - EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE - ), account( 'ddf32007-d3a5-4cad-9a20-b7793e96fdd2', diff --git a/modules/statics/src/base.ts b/modules/statics/src/base.ts index 77768263a8..dfa4c897a7 100644 --- a/modules/statics/src/base.ts +++ b/modules/statics/src/base.ts @@ -1964,6 +1964,82 @@ export enum UnderlyingAsset { 'hoodeth:usar' = 'hoodeth:usar', 'hoodeth:dram' = 'hoodeth:dram', 'hoodeth:week' = 'hoodeth:week', + 'hoodeth:aaoi' = 'hoodeth:aaoi', + 'hoodeth:amat' = 'hoodeth:amat', + 'hoodeth:apld' = 'hoodeth:apld', + 'hoodeth:arm' = 'hoodeth:arm', + 'hoodeth:asml' = 'hoodeth:asml', + 'hoodeth:asts' = 'hoodeth:asts', + 'hoodeth:avgo' = 'hoodeth:avgo', + 'hoodeth:ba' = 'hoodeth:ba', + 'hoodeth:baba' = 'hoodeth:baba', + 'hoodeth:cbrs' = 'hoodeth:cbrs', + 'hoodeth:ccl' = 'hoodeth:ccl', + 'hoodeth:celh' = 'hoodeth:celh', + 'hoodeth:clsk' = 'hoodeth:clsk', + 'hoodeth:coin' = 'hoodeth:coin', + 'hoodeth:cost' = 'hoodeth:cost', + 'hoodeth:crwd' = 'hoodeth:crwd', + 'hoodeth:ddog' = 'hoodeth:ddog', + 'hoodeth:dell' = 'hoodeth:dell', + 'hoodeth:elf' = 'hoodeth:elf', + 'hoodeth:ewy' = 'hoodeth:ewy', + 'hoodeth:f' = 'hoodeth:f', + 'hoodeth:flnc' = 'hoodeth:flnc', + 'hoodeth:futu' = 'hoodeth:futu', + 'hoodeth:gme' = 'hoodeth:gme', + 'hoodeth:glw' = 'hoodeth:glw', + 'hoodeth:inod' = 'hoodeth:inod', + 'hoodeth:intu' = 'hoodeth:intu', + 'hoodeth:ionq' = 'hoodeth:ionq', + 'hoodeth:iren' = 'hoodeth:iren', + 'hoodeth:lite' = 'hoodeth:lite', + 'hoodeth:lly' = 'hoodeth:lly', + 'hoodeth:lulu' = 'hoodeth:lulu', + 'hoodeth:lunr' = 'hoodeth:lunr', + 'hoodeth:mdb' = 'hoodeth:mdb', + 'hoodeth:mrvl' = 'hoodeth:mrvl', + 'hoodeth:mstr' = 'hoodeth:mstr', + 'hoodeth:mxl' = 'hoodeth:mxl', + 'hoodeth:nasa' = 'hoodeth:nasa', + 'hoodeth:nbis' = 'hoodeth:nbis', + 'hoodeth:nflx' = 'hoodeth:nflx', + 'hoodeth:nne' = 'hoodeth:nne', + 'hoodeth:nok' = 'hoodeth:nok', + 'hoodeth:nu' = 'hoodeth:nu', + 'hoodeth:now' = 'hoodeth:now', + 'hoodeth:nvts' = 'hoodeth:nvts', + 'hoodeth:p' = 'hoodeth:p', + 'hoodeth:peng' = 'hoodeth:peng', + 'hoodeth:poet' = 'hoodeth:poet', + 'hoodeth:pr' = 'hoodeth:pr', + 'hoodeth:qbts' = 'hoodeth:qbts', + 'hoodeth:qcom' = 'hoodeth:qcom', + 'hoodeth:qubt' = 'hoodeth:qubt', + 'hoodeth:rblx' = 'hoodeth:rblx', + 'hoodeth:rddt' = 'hoodeth:rddt', + 'hoodeth:rdw' = 'hoodeth:rdw', + 'hoodeth:rgti' = 'hoodeth:rgti', + 'hoodeth:rivn' = 'hoodeth:rivn', + 'hoodeth:rklb' = 'hoodeth:rklb', + 'hoodeth:rvi' = 'hoodeth:rvi', + 'hoodeth:sats' = 'hoodeth:sats', + 'hoodeth:shop' = 'hoodeth:shop', + 'hoodeth:smci' = 'hoodeth:smci', + 'hoodeth:sofi' = 'hoodeth:sofi', + 'hoodeth:soxx' = 'hoodeth:soxx', + 'hoodeth:spmo' = 'hoodeth:spmo', + 'hoodeth:tsem' = 'hoodeth:tsem', + 'hoodeth:tsm' = 'hoodeth:tsm', + 'hoodeth:ttwo' = 'hoodeth:ttwo', + 'hoodeth:umc' = 'hoodeth:umc', + 'hoodeth:ups' = 'hoodeth:ups', + 'hoodeth:wday' = 'hoodeth:wday', + 'hoodeth:xlk' = 'hoodeth:xlk', + 'hoodeth:xndu' = 'hoodeth:xndu', + 'hoodeth:xom' = 'hoodeth:xom', + 'hoodeth:zm' = 'hoodeth:zm', + 'hoodeth:zs' = 'hoodeth:zs', 'hemieth:hemi' = 'hemieth:hemi', 'hemieth:hemibtc' = 'hemieth:hemibtc', 'usdt0:stable' = 'usdt0:stable', diff --git a/modules/statics/src/coins.ts b/modules/statics/src/coins.ts index c7f49e2d30..260700137a 100644 --- a/modules/statics/src/coins.ts +++ b/modules/statics/src/coins.ts @@ -36,6 +36,7 @@ import { CoinMap } from './map'; import { BaseNetwork, getNetwork, getNetworksMap, NetworkType } from './networks'; import { getNetworkFeatures } from './networkFeatureMapForTokens'; import { ofcErc20Coins, tOfcErc20Coins } from './coins/ofcErc20Coins'; +import { ofcHoodethTokens } from './coins/ofcHoodethTokens'; import { ofcCoins } from './coins/ofcCoins'; import { allCoinsAndTokens } from './allCoinsAndTokens'; import { botOfcTokens } from './coins/botOfcTokens'; @@ -43,6 +44,7 @@ import { botOfcTokens } from './coins/botOfcTokens'; export const coins = CoinMap.fromCoins([ ...allCoinsAndTokens, ...ofcErc20Coins, + ...ofcHoodethTokens, ...tOfcErc20Coins, ...ofcCoins, ...botOfcTokens, diff --git a/modules/statics/src/coins/hoodethTokens.ts b/modules/statics/src/coins/hoodethTokens.ts new file mode 100644 index 0000000000..b661b7aa2c --- /dev/null +++ b/modules/statics/src/coins/hoodethTokens.ts @@ -0,0 +1,1034 @@ +import { erc20Token } from '../account'; +import { UnderlyingAsset } from '../base'; +import { EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE } from '../coinFeatures'; +import { Networks } from '../networks'; + +// Robinhood Chain (HoodETH) stock tokens - CECHO-1213 +export const hoodethTokens = [ + erc20Token( + '2597a273-8103-4e42-89cd-da241c231bc0', + 'hoodeth:amd', + 'Advanced Micro Devices, Inc.', + 18, + '0x86923f96303d656e4aa86d9d42d1e57ad2023fdc', + UnderlyingAsset['hoodeth:amd'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + '47c1c9fe-8904-4d3c-afb6-7b6070dd9d50', + 'hoodeth:baba', + 'Alibaba', + 18, + '0xad25ac6c84d497db898fa1e8387bf6af3532a1c4', + UnderlyingAsset['hoodeth:baba'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + '31baa733-4e2b-4fc6-82ea-2653cbd083ff', + 'hoodeth:googl', + 'Alphabet Inc.', + 18, + '0x2e0847e8910a9732eb3fb1bb4b70a580adad4fe3', + UnderlyingAsset['hoodeth:googl'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + '6852e072-48ec-41e7-b0ad-c8594c560f5c', + 'hoodeth:amzn', + 'Amazon.com, Inc.', + 18, + '0x12f190a9f9d7d37a250758b26824b97ce941bf54', + UnderlyingAsset['hoodeth:amzn'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + 'da8769ad-98d5-4349-bea9-8fee3be83c9a', + 'hoodeth:aapl', + 'Apple Inc.', + 18, + '0xaf3d76f1834a1d425780943c99ea8a608f8a93f9', + UnderlyingAsset['hoodeth:aapl'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + '58de73d2-1337-4c9f-b936-e9d250104744', + 'hoodeth:apld', + 'Applied Digital', + 18, + '0xb8dbf92f9741c9ac1c32115e78581f23509916fd', + UnderlyingAsset['hoodeth:apld'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + '4e7ca8a7-ee2d-4a39-9bb8-96aca2da5fbf', + 'hoodeth:amat', + 'Applied Materials', + 18, + '0x36046893810a7e7fce501229d57dc3fc8c8716d0', + UnderlyingAsset['hoodeth:amat'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + 'bd7463c6-df6c-46ac-9c4b-2ec87e10d1f1', + 'hoodeth:aaoi', + 'Applied Optoelectronics', + 18, + '0x521cf887e6531c6f667b5bc4d896e5d9bfe8eb2e', + UnderlyingAsset['hoodeth:aaoi'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + 'cf303c78-5738-4b67-86f7-14a44b3d2515', + 'hoodeth:arm', + 'Arm Holdings plc', + 18, + '0x666716999e75d2652398ff830bbc2e485946e140', + UnderlyingAsset['hoodeth:arm'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + '7545f551-bf07-41f1-9396-97544813dfd0', + 'hoodeth:asml', + 'ASML Holding NV', + 18, + '0x47f93d52cbec7c6d2cfc080e154002370a60daea', + UnderlyingAsset['hoodeth:asml'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + 'f3a07447-c8e4-4806-9db5-ebbfc7a077b0', + 'hoodeth:asts', + 'AST SpaceMobile', + 18, + '0x1af6446f07eb1d97c546afc8c9544cbdf3ad5137', + UnderlyingAsset['hoodeth:asts'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + 'ffb9fa51-e51b-40f0-a2c9-3f735bab7d7c', + 'hoodeth:be', + 'Bloom Energy Corporation', + 18, + '0x822cc93ffd030293e9842c30bbd678f530701867', + UnderlyingAsset['hoodeth:be'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + '9c1fa204-5ba9-42a4-9dbd-52382a7b6a03', + 'hoodeth:ba', + 'Boeing', + 18, + '0x4d21483a44bf67a86b77e3da301411880797d452', + UnderlyingAsset['hoodeth:ba'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + '9d7ae82e-0452-4f84-a395-0cc26aa7281b', + 'hoodeth:avgo', + 'Broadcom', + 18, + '0x156e175dd063a8ce274c50654ef40e0032b3fbcf', + UnderlyingAsset['hoodeth:avgo'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + '1c9fdfbc-6633-4b64-9155-5a868dedb978', + 'hoodeth:ccl', + 'Carnival Corporation', + 18, + '0x9651342cea770ae9a2969ba2a52611523146aef9', + UnderlyingAsset['hoodeth:ccl'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + '7382f839-5584-417e-b9a7-5d96425e0afd', + 'hoodeth:celh', + 'Celsius', + 18, + '0x8cf07c5a878945185d327aaa6e33faa95f95e7bf', + UnderlyingAsset['hoodeth:celh'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + '3bbd36d8-db82-4c07-8df9-00f3d58f9872', + 'hoodeth:cbrs', + 'Cerebras Systems', + 18, + '0x5c90450bbb4273d7b2f17cf6917aeb237a569679', + UnderlyingAsset['hoodeth:cbrs'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + 'c7491c49-a509-4791-aa18-2ab12130c518', + 'hoodeth:crcl', + 'Circle Internet Group Inc', + 18, + '0xdf0992e440dd0be65bd8439b609d6d4366bf1cb5', + UnderlyingAsset['hoodeth:crcl'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + '24dec65f-d2f5-433c-bf6e-a3840b96e950', + 'hoodeth:clsk', + 'CleanSpark', + 18, + '0xcbb95bbf36099d34da091dc6fa6f49efa257cee3', + UnderlyingAsset['hoodeth:clsk'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + '22bffb7c-8a2c-4ed4-84ae-7a8e2e0734e2', + 'hoodeth:coin', + 'Coinbase', + 18, + '0x6330d8c3178a418788df01a47479c0ce7ccf450b', + UnderlyingAsset['hoodeth:coin'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + '9c12894d-62c6-4e9c-bea7-9d909a23c496', + 'hoodeth:crwv', + 'CoreWeave', + 18, + '0x5f10a1c971b69e47e059e1dc91901b59b3fb49c3', + UnderlyingAsset['hoodeth:crwv'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + '5f160ec9-f074-4bf3-a7ef-9b9273fce1fe', + 'hoodeth:glw', + 'Corning', + 18, + '0x7c04e6a3368f2a1de3874f0e80d2e0a1a9915da6', + UnderlyingAsset['hoodeth:glw'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + 'caa02fb7-45ba-4af2-a0b3-827a2f550b72', + 'hoodeth:cost', + 'Costco', + 18, + '0x4ea005168d7f09a7a0ba9d1def21a479950e44c2', + UnderlyingAsset['hoodeth:cost'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + '764b6de2-2eea-4c5e-90ec-99f76984c828', + 'hoodeth:crwd', + 'CrowdStrike Holdings', + 18, + '0xea72ecca2d0f6bfa1394dbbcff85b52cd4233931', + UnderlyingAsset['hoodeth:crwd'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + 'ed88737d-e4e6-4721-b227-59dc307794a3', + 'hoodeth:qbts', + 'D-Wave Quantum', + 18, + '0xc583c60aef9dc401da72cec1b404743a93cea1cc', + UnderlyingAsset['hoodeth:qbts'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + '7a839eb5-a23e-4894-b0c6-d482b8da5044', + 'hoodeth:ddog', + 'Datadog', + 18, + '0x27c99fbde9d0d2aa4f4bfb4943f237843ddf6958', + UnderlyingAsset['hoodeth:ddog'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + '7e955555-9504-4173-b9a6-2799b88e38f0', + 'hoodeth:dram', + 'Dataram Corporation', + 18, + '0x33c18e2cc8ae9ae486e785090d86b2ce632ff994', + UnderlyingAsset['hoodeth:dram'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + 'b09de291-c9d8-405f-98fd-3b0ca9096186', + 'hoodeth:dell', + 'Dell', + 18, + '0x941ae714ec6d8130c7b75d67160ca08f1e7d11dd', + UnderlyingAsset['hoodeth:dell'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + '6cca71d6-1d1c-4822-a050-dea1f2c9a73b', + 'hoodeth:elf', + 'e.l.f. Beauty', + 18, + '0x39ec44bee4f6a116c6f9b8de566848a985c53c60', + UnderlyingAsset['hoodeth:elf'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + 'b76595b5-8c3a-4acc-8dad-54ee025fe972', + 'hoodeth:sats', + 'EchoStar', + 18, + '0x95052ddcd5dc25641657424a8cf04834997e1730', + UnderlyingAsset['hoodeth:sats'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + '7fb04506-28aa-41e3-b2db-4b5d38065eff', + 'hoodeth:lly', + 'Eli Lilly', + 18, + '0x8005d266423c7ea827372c9c864491e5786600ea', + UnderlyingAsset['hoodeth:lly'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + 'deb752dc-726b-42e9-8ffb-07c55cfd261b', + 'hoodeth:p', + 'Everpure', + 18, + '0x1cdad396db64bda184d5182a97dd9b3c62100b7d', + UnderlyingAsset['hoodeth:p'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + '57c5305e-364b-4e17-8910-f9cc96e036c3', + 'hoodeth:xom', + 'Exxon Mobil', + 18, + '0xf9b46d3d1b22199d4d1025a9cedb540a33f1a2d5', + UnderlyingAsset['hoodeth:xom'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + '5efde6dc-176a-4a21-bd1f-f88f6607d088', + 'hoodeth:flnc', + 'Fluence Energy', + 18, + '0x282e87451e10fa6679bc7d76c69be44cd3fc777c', + UnderlyingAsset['hoodeth:flnc'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + '6e4eca40-dfec-4c55-a04d-39ce43f9ba20', + 'hoodeth:f', + 'Ford Motor', + 18, + '0x25c288e6d899b9bc30160965ad9644c67e73be0c', + UnderlyingAsset['hoodeth:f'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + '36cd2763-2402-44a7-b5bc-f985b3a74f76', + 'hoodeth:futu', + 'Futu Holdings', + 18, + '0xeb30663bdff0622ef4e4e5cbb4e975f19f33f51d', + UnderlyingAsset['hoodeth:futu'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + '33be21dd-9315-4139-9f1a-0f1c738e3699', + 'hoodeth:gme', + 'GameStop', + 18, + '0x1b0e319c6a659f002271b69db8a7df2f911c153e', + UnderlyingAsset['hoodeth:gme'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + '7be6dd43-5273-42b6-8a67-63d7650a2d72', + 'hoodeth:inod', + 'Innodata', + 18, + '0xf1953dab6fad537488d5a022361ffaa8b4c95ec6', + UnderlyingAsset['hoodeth:inod'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + '8aa38d75-8f24-4fe4-8642-072e3e0529bd', + 'hoodeth:intc', + 'Intel Corporation', + 18, + '0xc72b96e0e48ecd4dc75e1e45396e26300bc39681', + UnderlyingAsset['hoodeth:intc'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + '2bc23c79-4bce-47d3-89c9-1b21d49dea63', + 'hoodeth:intu', + 'Intuit', + 18, + '0x56d23bee5f41a7120170b0c603dae30128e460e9', + UnderlyingAsset['hoodeth:intu'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + '36b30d22-1fdf-45ee-ae32-9162a2f8d9f0', + 'hoodeth:lunr', + 'Intuitive Machines', + 18, + '0xa5d4968421ba94814be3b136b15cf422101ac1a3', + UnderlyingAsset['hoodeth:lunr'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + '92f0c908-efa9-490a-a344-15511d54ce57', + 'hoodeth:qqq', + 'Invesco QQQ Trust', + 18, + '0xd5f3879160bc7c32ebb4dc785f8a4f505888de68', + UnderlyingAsset['hoodeth:qqq'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + 'e9a5d68d-12a6-49a8-9d6a-165ed226bb84', + 'hoodeth:spmo', + 'Invesco S&P 500 Momentum ETF', + 18, + '0xad622320e520de39e72d41ef07438c3fd3354875', + UnderlyingAsset['hoodeth:spmo'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + 'd438050e-10b8-48f8-854c-932ab589eb53', + 'hoodeth:ionq', + 'IonQ', + 18, + '0x558378e000d634a36593e338ebacdd6207640efe', + UnderlyingAsset['hoodeth:ionq'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + '94d2f2b1-7311-47b0-814e-d928e69e2de8', + 'hoodeth:iren', + 'IREN Limited', + 18, + '0xf0ab0c93be6f41369d302e55db1a96b3c430212d', + UnderlyingAsset['hoodeth:iren'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + 'be7d01f7-12e9-4e19-91be-56d894c9fa9c', + 'hoodeth:sgov', + 'iShares 0-3 Month Treasury Bond ETF', + 18, + '0x92fd66527192e3e61d4ddd13322aa222de86f9b5', + UnderlyingAsset['hoodeth:sgov'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + '90bbef61-423a-4a28-a6e6-bd9009fbc93e', + 'hoodeth:ewy', + 'iShares MSCI South Korea fund', + 18, + '0x7f0abef0c07280f82c6a08ead09ded6bae2c13fc', + UnderlyingAsset['hoodeth:ewy'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + 'fdbdd87b-e67d-491f-8d51-7534090d9857', + 'hoodeth:soxx', + 'iShares Semiconductor ETF', + 18, + '0x75742c18bc1f1c5c5f448f4c9d9c6f66dafaaa38', + UnderlyingAsset['hoodeth:soxx'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + '326f35b5-d998-4851-81b3-fcb5c0ccf0ec', + 'hoodeth:slv', + 'iShares Silver Trust', + 18, + '0x411efb0e7f985935daec3d4c3ebaea0d0ad7d89f', + UnderlyingAsset['hoodeth:slv'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + '18e487a4-d55a-4875-bde4-3aeb34cb3f1b', + 'hoodeth:lulu', + 'Lululemon', + 18, + '0x4e62068525ab11fe768e29dfd00ef909b9803016', + UnderlyingAsset['hoodeth:lulu'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + '90a9b87d-6a35-4825-b5d0-29d1405f494d', + 'hoodeth:lite', + 'Lumentum', + 18, + '0x8ef20885f94e3d9bc7eb3080279188bd5ed7c08c', + UnderlyingAsset['hoodeth:lite'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + '4929fd2c-ecf9-472a-9dcb-c48afc66fb5e', + 'hoodeth:mrvl', + 'Marvell Technology', + 18, + '0x62fd0668e10d8b72339be2dcf7643001688ff13b', + UnderlyingAsset['hoodeth:mrvl'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + '6c88bc15-c425-40ae-85a2-af9bf6e0966c', + 'hoodeth:mxl', + 'MaxLinear', + 18, + '0x48961813349333209994750ffa89b3c5c22ec969', + UnderlyingAsset['hoodeth:mxl'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + 'df3845bb-bab1-49d4-a040-c81a768d244a', + 'hoodeth:meta', + 'Meta Platforms, Inc.', + 18, + '0xc0d6457c16cc70d6790dd43521c899c87ce02f35', + UnderlyingAsset['hoodeth:meta'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + '13ebc2ae-5bee-4433-b5f1-94d823c0db20', + 'hoodeth:mu', + 'Micron Technology', + 18, + '0xff080c8ce2e5feadaca0da81314ae59d232d4afd', + UnderlyingAsset['hoodeth:mu'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + '49c855b1-0d85-4f79-9756-7ecf28440225', + 'hoodeth:msft', + 'Microsoft Corporation', + 18, + '0xe93237c50d904957cf27e7b1133b510c669c2e74', + UnderlyingAsset['hoodeth:msft'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + 'c4558e1c-6a89-4e16-9c87-72f89de9caea', + 'hoodeth:mdb', + 'MongoDB', + 18, + '0xddf2266b79abf0b48898959b0ed6e6adf512be74', + UnderlyingAsset['hoodeth:mdb'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + 'bd2d8f22-d16a-4849-8cec-c79c26730c88', + 'hoodeth:nne', + 'Nano Nuclear Energy', + 18, + '0xbef75684c43c4ea7bd18dd532a2244674ee8b926', + UnderlyingAsset['hoodeth:nne'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + 'f1b05e08-a29c-40cc-886b-8fcaa4208a1c', + 'hoodeth:nvts', + 'Navitas Semiconductor', + 18, + '0xbe6702d7b70315376dc48a3293f24f0982f86386', + UnderlyingAsset['hoodeth:nvts'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + '0f93c0dc-17b2-4362-a8e3-4162ad3f0ec9', + 'hoodeth:nbis', + 'Nebius Group', + 18, + '0x9d9c6684f596f66a64c030b93a886d51fd4d7931', + UnderlyingAsset['hoodeth:nbis'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + '2f603edd-ef7e-471f-87f0-6203e0ea381b', + 'hoodeth:nflx', + 'Netflix', + 18, + '0xe0444ef8bf4ed74f74fd73686e2ddf4c1c5591e8', + UnderlyingAsset['hoodeth:nflx'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + '5898820c-7d6f-47d8-9ff6-4786891a7bcd', + 'hoodeth:nok', + 'Nokia', + 18, + '0x25ee805ac369b6e3f8bf5764c682d34a37cb7175', + UnderlyingAsset['hoodeth:nok'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + '9d8234a1-12cb-4a87-845b-28f03c70e92f', + 'hoodeth:nu', + 'Nu', + 18, + '0x408c14038a04f7bd235329e26d2bf569ee20e250', + UnderlyingAsset['hoodeth:nu'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + 'eada9a10-3d17-465f-978b-5a183e73f4c6', + 'hoodeth:nvda', + 'NVIDIA Corporation', + 18, + '0xd0601ce157db5bdc3162bbac2a2c8af5320d9eec', + UnderlyingAsset['hoodeth:nvda'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + '0519b754-a119-4431-9d00-490522e2a4d1', + 'hoodeth:orcl', + 'Oracle Corporation', + 18, + '0xb0992820e760d836549ba69bc7598b4af75dee03', + UnderlyingAsset['hoodeth:orcl'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + 'f4e2c9a2-130f-4080-ba53-d43d5494ee2b', + 'hoodeth:pltr', + 'Palantir Technologies Inc.', + 18, + '0x894e1ec2d74ffe5aef8dc8a9e84686accb964f2a', + UnderlyingAsset['hoodeth:pltr'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + '36707ef0-d481-497e-97e3-41e0ccc4ed40', + 'hoodeth:peng', + 'Penguin Solutions', + 18, + '0x9b23573b156b52565012f5ce02cdf60afbaa70be', + UnderlyingAsset['hoodeth:peng'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + '1eaaf957-8f86-40fc-9b17-8a784cb616fb', + 'hoodeth:pr', + 'Permian Resources', + 18, + '0x4189f0c66ebbb0bfef1c31f763131361ef32f77c', + UnderlyingAsset['hoodeth:pr'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + '66fbc7cd-9c90-42e5-89e0-0e60c3a47159', + 'hoodeth:poet', + 'POET Technologies', + 18, + '0xcf6b2d875361be807eafa57458c80f28521f9333', + UnderlyingAsset['hoodeth:poet'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + '32fe957d-c234-4566-b286-ed197950d811', + 'hoodeth:qcom', + 'Qualcomm', + 18, + '0x0f17206447090e464c277571124dd2688e48aea9', + UnderlyingAsset['hoodeth:qcom'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + 'ec67a659-b880-42ec-8ecb-229b9e41a9ed', + 'hoodeth:qubt', + 'Quantum Computing, Inc', + 18, + '0x59818904ab4ce163b3ce4ffb64f2d6ca02c434b4', + UnderlyingAsset['hoodeth:qubt'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + '8dbb4221-e7f9-452d-9a52-2993f12bb3dd', + 'hoodeth:rddt', + 'Reddit', + 18, + '0x05b37fb53a299a1b874a619e1c4c404d52c36f4c', + UnderlyingAsset['hoodeth:rddt'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + '8e2781f1-5f63-4d1d-86f7-93a66a4cf407', + 'hoodeth:rdw', + 'Redwire', + 18, + '0x92ef19e82bd8ff36661de838d5eae7e5cef0effe', + UnderlyingAsset['hoodeth:rdw'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + '19ec73dc-8c6a-4a7b-87c4-7b55104a75ee', + 'hoodeth:rgti', + 'Rigetti Computing', + 18, + '0x284358abc07f9359f19f4b5b4ac91901be2597ba', + UnderlyingAsset['hoodeth:rgti'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + '8069f925-3f3f-4ace-8941-29e5737d4c19', + 'hoodeth:rivn', + 'Rivian Automotive', + 18, + '0xb1bf26c1d20ff267a4f93550d1e0d06ac40a114b', + UnderlyingAsset['hoodeth:rivn'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + 'bd91e41f-94db-4513-bcdf-4c3e4ee69257', + 'hoodeth:rblx', + 'Roblox', + 18, + '0xf0c4bf4c582cb3836e98394b1d4e7b7281101be8', + UnderlyingAsset['hoodeth:rblx'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + '7f2bf2e8-98a9-4219-b691-7b4235b22909', + 'hoodeth:rklb', + 'Rocket Lab Corporation', + 18, + '0x3b14c39e89d60d627b42a1a4ca45b5bb45fc12e2', + UnderlyingAsset['hoodeth:rklb'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + '8b56fae8-badd-41e8-bde6-db9efd64b50a', + 'hoodeth:rvi', + 'RVI', + 18, + '0xb02e3e1b7f68559427c2d9100566e4f3cc5b7611', + UnderlyingAsset['hoodeth:rvi'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + 'b3a25405-760f-4578-bbe8-14ddfbe3aacd', + 'hoodeth:sndk', + 'SanDisk', + 18, + '0xb90a19ff0af67f7779aff50a882a9cff42446400', + UnderlyingAsset['hoodeth:sndk'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + '19723e25-53be-4213-8d70-d203b7f607c5', + 'hoodeth:now', + 'ServiceNow', + 18, + '0x0c3260af4b8f13a69c4c2dfb84fd667890cdfa14', + UnderlyingAsset['hoodeth:now'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + '8f6274aa-7dca-4b22-b6ce-b2aa46046a4f', + 'hoodeth:shop', + 'Shopify', + 18, + '0xf53f66751b1eff985311b693531e3290f600c410', + UnderlyingAsset['hoodeth:shop'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + '894c579e-328f-496e-a4d6-f6a3486a177d', + 'hoodeth:sofi', + 'SoFi Technologies', + 18, + '0x98e75885157c80992a8d41b696d8c9c6fb30a926', + UnderlyingAsset['hoodeth:sofi'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + 'e3613375-49ff-4279-9c57-e5fb9728a931', + 'hoodeth:spy', + 'SPDR S&P 500 ETF Trust', + 18, + '0x117cc2133c37b721f49de2a7a74833232b3b4c0c', + UnderlyingAsset['hoodeth:spy'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + '760f1041-7aad-4fcb-9c5e-bb9bc92d90d6', + 'hoodeth:xlk', + 'State Street Technology Select Sector SPDR ETF', + 18, + '0x15cd20759ce7f3285c29a319de2d1a2e098c6f43', + UnderlyingAsset['hoodeth:xlk'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + '07b9f60b-c690-47c3-8bb9-1d60670eddea', + 'hoodeth:mstr', + 'Strategy Inc.', + 18, + '0xec262a75e413fafd0df80480274532c79d42da09', + UnderlyingAsset['hoodeth:mstr'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + 'c3c305bc-9bfe-40be-95fd-8262f2488fac', + 'hoodeth:smci', + 'Super Micro Computer', + 18, + '0xc01aa1fecec0605b13bc84874ff7256c0f5f562a', + UnderlyingAsset['hoodeth:smci'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + '3f76a5f6-32a0-4f99-abdb-6b32a6b10163', + 'hoodeth:tsm', + 'Taiwan Semiconductor Manufacturing', + 18, + '0x58ffe4a942d3885baa22d7520691f611ef09e7aa', + UnderlyingAsset['hoodeth:tsm'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + '0f996c54-d540-4006-b61d-706a3b234a3c', + 'hoodeth:ttwo', + 'Take-Two Interactive Software', + 18, + '0x5e81213613b6b86eab4c6c50d718d34359459786', + UnderlyingAsset['hoodeth:ttwo'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + 'b60c70d4-69d5-461e-8dc6-955184c7c3f1', + 'hoodeth:nasa', + 'Tema Space Innovators ETF', + 18, + '0x6ddb95405db6179012bff2fff7e0f8d49cf00137', + UnderlyingAsset['hoodeth:nasa'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + '71bfcfbb-0e1d-4712-9836-8e7c481b9d87', + 'hoodeth:tsla', + 'Tesla', + 18, + '0x322f0929c4625ed5bad873c95208d54e1c003b2d', + UnderlyingAsset['hoodeth:tsla'], + Networks.main.hoodeth + ), + erc20Token( + 'e21a1de5-b81d-4b17-97dc-af8d30fd9db2', + 'hoodeth:tsem', + 'Tower Semiconductor', + 18, + '0x89776d4cd68193597a2fc132cfac1fde36ccea8a', + UnderlyingAsset['hoodeth:tsem'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + 'c403a16c-6f8e-4ac4-9504-f9cc810f90de', + 'hoodeth:umc', + 'United Microelectronics', + 18, + '0x0e6e67ba88e7b5d9b67636a215c76779b948de79', + UnderlyingAsset['hoodeth:umc'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + 'f6f0a6f8-fea8-4306-baaf-c88a96b071de', + 'hoodeth:uso', + 'United States Oil Fund, LP', + 18, + '0xa30fa36db767ad9ed3f7a60fc79526fb4d56d344', + UnderlyingAsset['hoodeth:uso'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + '10538982-e986-4745-bdd4-dc3833754316', + 'hoodeth:ups', + 'UPS', + 18, + '0xf23250dac154d05bb671cb0d0ebef3c635c79ce2', + UnderlyingAsset['hoodeth:ups'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + '7d0446f2-929f-4bea-b9b7-cb9f40dc0786', + 'hoodeth:usar', + 'USA Rare Earth Inc.', + 18, + '0xd917b029c761d264c6a312bbbcda868658ef86a6', + UnderlyingAsset['hoodeth:usar'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + 'ca3ac76e-e88f-4f39-8571-006cd9b97389', + 'hoodeth:usdg', + 'USDG', + 18, + '0x5fc5360d0400a0fd4f2af552add042d716f1d168', + UnderlyingAsset['hoodeth:usdg'], + Networks.main.hoodeth + ), + erc20Token( + 'ea09a0ff-059b-4527-8e7b-75dfc750218b', + 'hoodeth:week', + 'Weekly T-Bill ETF', + 18, + '0xc93a8c440cea26d7445df01729f193b27965099f', + UnderlyingAsset['hoodeth:week'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + 'f694acee-378c-463e-980c-96d9a746a06d', + 'hoodeth:wday', + 'Workday', + 18, + '0x82da4646242e1d962e96e932269dc644c94a9caa', + UnderlyingAsset['hoodeth:wday'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + 'd6fef21f-be3c-48c4-a76b-20b1bc054761', + 'hoodeth:xndu', + 'Xanadu Quantum', + 18, + '0xa8eb3bccbf2017ee7cbfb652eb51cf2e1b153289', + UnderlyingAsset['hoodeth:xndu'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + 'd9230584-28ac-46a2-8d7c-4be8c67ecdb1', + 'hoodeth:zm', + 'Zoom', + 18, + '0x44c4f142009036cf477ed2d09932051843137cf1', + UnderlyingAsset['hoodeth:zm'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + 'd10286e7-0226-4bb2-b414-bf29ba9d5111', + 'hoodeth:zs', + 'Zscaler', + 18, + '0x7dc013eb55e436f30d7ed1afe4e36d6e45e3c3f7', + UnderlyingAsset['hoodeth:zs'], + Networks.main.hoodeth, + EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE + ), + erc20Token( + '3493d608-fd3e-45dc-926d-783d54a8fe4d', + 'thoodeth:amzn', + 'Amazon', + 18, + '0x5884ad2f920c162cfbbacc88c9c51aa75ec09e02', + UnderlyingAsset['thoodeth:amzn'], + Networks.test.hoodeth + ), + erc20Token( + '8ede8dbd-1fa6-4669-be6d-6b19b3c98766', + 'thoodeth:tsla', + 'Tesla', + 18, + '0xc9f9c86933092bbbfff3ccb4b105a4a94bf3bd4e', + UnderlyingAsset['thoodeth:tsla'], + Networks.test.hoodeth + ), +]; diff --git a/modules/statics/src/coins/ofcErc20Coins.ts b/modules/statics/src/coins/ofcErc20Coins.ts index 92259b3d56..19fb115e0d 100644 --- a/modules/statics/src/coins/ofcErc20Coins.ts +++ b/modules/statics/src/coins/ofcErc20Coins.ts @@ -35,34 +35,6 @@ export const ofcErc20Coins = [ ofcerc20('bd61426f-87d8-4c52-b47d-2dc5eed84f64', 'ofcnmr', 'Numeraire', 18, UnderlyingAsset.NMR), ofcerc20('3db3b895-756c-4c95-9dea-08d283f09a7a', 'ofcmeme', 'Meme', 8, UnderlyingAsset.MEME), ofcerc20('acf1a5a3-4555-4aa2-8c80-4e2cd4cdb61c', 'ofceth:meme', 'meme', 18, underlyingAssetForSymbol('eth:meme')), - ofcerc20( - '1806a1c6-56b1-4107-89af-8c93159905b9', - 'ofchoodeth:tsla', - 'Tesla', - 18, - UnderlyingAsset['hoodeth:tsla'], - undefined, - undefined, - undefined, - undefined, - undefined, - true, - 'hoodeth' - ), - ofcerc20( - '4099ae4f-a6d0-4ee5-b26b-029cdd93645a', - 'ofchoodeth:usdg', - 'USDG', - 18, - UnderlyingAsset['hoodeth:usdg'], - undefined, - undefined, - undefined, - undefined, - undefined, - true, - 'hoodeth' - ), ofcerc20( '66c849fa-ee81-4932-8ab7-d6b5d1f5b8da', 'ofchemieth:hemi', @@ -4149,330 +4121,6 @@ export const ofcErc20Coins = [ 'xtzevm' ), - // Robinhood Chain tokens - CECHO-1183 - ofcerc20( - '9c927821-eed5-45d6-900e-0d6a119a5f6d', - 'ofchoodeth:nvda', - 'NVIDIA Corporation', - 18, - underlyingAssetForSymbol('hoodeth:nvda'), - undefined, - undefined, - '', - undefined, - undefined, - true, - 'hoodeth' - ), - ofcerc20( - 'ae355bf8-423c-4bd7-90ba-fa41664927a9', - 'ofchoodeth:mu', - 'Micron Technology', - 18, - underlyingAssetForSymbol('hoodeth:mu'), - undefined, - undefined, - '', - undefined, - undefined, - true, - 'hoodeth' - ), - ofcerc20( - '20ab47b6-f825-4ec3-bc9e-1173588676c4', - 'ofchoodeth:sndk', - 'SanDisk', - 18, - underlyingAssetForSymbol('hoodeth:sndk'), - undefined, - undefined, - '', - undefined, - undefined, - true, - 'hoodeth' - ), - ofcerc20( - 'e141dac4-9eb6-477e-b3ca-753cca3dac0d', - 'ofchoodeth:amd', - 'Advanced Micro Devices, Inc.', - 18, - underlyingAssetForSymbol('hoodeth:amd'), - undefined, - undefined, - '', - undefined, - undefined, - true, - 'hoodeth' - ), - ofcerc20( - 'c35c8bbc-84c2-4fbd-9371-fe0098bdb49c', - 'ofchoodeth:spy', - 'SPDR S&P 500 ETF Trust', - 18, - underlyingAssetForSymbol('hoodeth:spy'), - undefined, - undefined, - '', - undefined, - undefined, - true, - 'hoodeth' - ), - ofcerc20( - '2610fc79-c281-4624-9975-c2b23f0e4ed1', - 'ofchoodeth:msft', - 'Microsoft Corporation', - 18, - underlyingAssetForSymbol('hoodeth:msft'), - undefined, - undefined, - '', - undefined, - undefined, - true, - 'hoodeth' - ), - ofcerc20( - '2c7e60eb-f9dc-48cd-b169-2acd0fef56e5', - 'ofchoodeth:pltr', - 'Palantir Technologies Inc.', - 18, - underlyingAssetForSymbol('hoodeth:pltr'), - undefined, - undefined, - '', - undefined, - undefined, - true, - 'hoodeth' - ), - ofcerc20( - '3fbefdcc-930a-4795-8ceb-824d54a0c4e2', - 'ofchoodeth:intc', - 'Intel Corporation', - 18, - underlyingAssetForSymbol('hoodeth:intc'), - undefined, - undefined, - '', - undefined, - undefined, - true, - 'hoodeth' - ), - ofcerc20( - 'a607c169-9b53-4d91-b4a7-0a4e4c0604df', - 'ofchoodeth:qqq', - 'Invesco QQQ Trust', - 18, - underlyingAssetForSymbol('hoodeth:qqq'), - undefined, - undefined, - '', - undefined, - undefined, - true, - 'hoodeth' - ), - ofcerc20( - '33ba08c6-75bd-4b76-b158-a2b6505ce48b', - 'ofchoodeth:slv', - 'iShares Silver Trust', - 18, - underlyingAssetForSymbol('hoodeth:slv'), - undefined, - undefined, - '', - undefined, - undefined, - true, - 'hoodeth' - ), - ofcerc20( - '99f4f2e1-39c7-4422-a751-4e02af80600c', - 'ofchoodeth:crcl', - 'Circle Internet Group Inc', - 18, - underlyingAssetForSymbol('hoodeth:crcl'), - undefined, - undefined, - '', - undefined, - undefined, - true, - 'hoodeth' - ), - ofcerc20( - 'bf014246-5a76-4b2d-a351-c989506eea95', - 'ofchoodeth:meta', - 'Meta Platforms, Inc.', - 18, - underlyingAssetForSymbol('hoodeth:meta'), - undefined, - undefined, - '', - undefined, - undefined, - true, - 'hoodeth' - ), - ofcerc20( - '728d1e53-f757-4072-88aa-bb2488192f5a', - 'ofchoodeth:aapl', - 'Apple Inc.', - 18, - underlyingAssetForSymbol('hoodeth:aapl'), - undefined, - undefined, - '', - undefined, - undefined, - true, - 'hoodeth' - ), - ofcerc20( - '1afcbe16-afd1-41ba-b666-a128cbd08d0b', - 'ofchoodeth:googl', - 'Alphabet Inc.', - 18, - underlyingAssetForSymbol('hoodeth:googl'), - undefined, - undefined, - '', - undefined, - undefined, - true, - 'hoodeth' - ), - ofcerc20( - '7ca62551-0f32-47c2-9e35-a55f5a782ed6', - 'ofchoodeth:uso', - 'United States Oil Fund, LP', - 18, - underlyingAssetForSymbol('hoodeth:uso'), - undefined, - undefined, - '', - undefined, - undefined, - true, - 'hoodeth' - ), - ofcerc20( - 'ae54304e-e45c-4d55-a091-3e53650b043d', - 'ofchoodeth:amzn', - 'Amazon.com, Inc.', - 18, - underlyingAssetForSymbol('hoodeth:amzn'), - undefined, - undefined, - '', - undefined, - undefined, - true, - 'hoodeth' - ), - ofcerc20( - '0c879212-b6fa-4ffa-98b1-725afc63784c', - 'ofchoodeth:crwv', - 'CoreWeave', - 18, - underlyingAssetForSymbol('hoodeth:crwv'), - undefined, - undefined, - '', - undefined, - undefined, - true, - 'hoodeth' - ), - ofcerc20( - '564305d1-da1d-401b-bd56-9203b823e60c', - 'ofchoodeth:orcl', - 'Oracle Corporation', - 18, - underlyingAssetForSymbol('hoodeth:orcl'), - undefined, - undefined, - '', - undefined, - undefined, - true, - 'hoodeth' - ), - ofcerc20( - '53cbf6aa-a7e4-42e9-abae-2201a39238e9', - 'ofchoodeth:sgov', - 'iShares 0-3 Month Treasury Bond ETF', - 18, - underlyingAssetForSymbol('hoodeth:sgov'), - undefined, - undefined, - '', - undefined, - undefined, - true, - 'hoodeth' - ), - ofcerc20( - 'ec1fb872-22e4-4b82-a7e7-ce876c4f1316', - 'ofchoodeth:be', - 'Bloom Energy Corporation', - 18, - underlyingAssetForSymbol('hoodeth:be'), - undefined, - undefined, - '', - undefined, - undefined, - true, - 'hoodeth' - ), - ofcerc20( - 'd1f75f57-9f62-4c30-bfd9-8ed76884f884', - 'ofchoodeth:usar', - 'USA Rare Earth Inc.', - 18, - underlyingAssetForSymbol('hoodeth:usar'), - undefined, - undefined, - '', - undefined, - undefined, - true, - 'hoodeth' - ), - ofcerc20( - '15dcb80c-b313-4540-86db-1a3883e80f7b', - 'ofchoodeth:dram', - 'Dataram Corporation', - 18, - underlyingAssetForSymbol('hoodeth:dram'), - undefined, - undefined, - '', - undefined, - undefined, - true, - 'hoodeth' - ), - ofcerc20( - '43fb356f-8b1e-4569-b6b8-982f8bfb0465', - 'ofchoodeth:week', - 'Weekly T-Bill ETF', - 18, - underlyingAssetForSymbol('hoodeth:week'), - undefined, - undefined, - '', - undefined, - undefined, - true, - 'hoodeth' - ), - ofcerc20( 'e870395c-a5e0-41b2-b900-6bd1a1a38088', 'ofcchiliz:acm', @@ -5228,34 +4876,6 @@ export const tOfcErc20Coins = [ undefined, 'hteth' ), - tofcerc20( - 'df59331f-b380-4fb7-ad9a-0e0c0c3ec730', - 'ofcthoodeth:amzn', - 'Amazon', - 18, - UnderlyingAsset['thoodeth:amzn'], - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - 'thoodeth' - ), - tofcerc20( - '6abb714a-de88-4181-988e-86fbbfbc9c2e', - 'ofcthoodeth:tsla', - 'Tesla', - 18, - UnderlyingAsset['thoodeth:tsla'], - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - 'thoodeth' - ), ofcerc20( 'cc2a92cf-d799-463b-b08c-e9a4d5e87934', diff --git a/modules/statics/src/coins/ofcHoodethTokens.ts b/modules/statics/src/coins/ofcHoodethTokens.ts new file mode 100644 index 0000000000..ca229707af --- /dev/null +++ b/modules/statics/src/coins/ofcHoodethTokens.ts @@ -0,0 +1,1448 @@ +import { ofcerc20, tofcerc20 } from '../ofc'; +import { UnderlyingAsset } from '../base'; + +// OFC equivalents of Robinhood Chain (HoodETH) stock tokens - CECHO-1213 +export const ofcHoodethTokens = [ + ofcerc20( + 'e141dac4-9eb6-477e-b3ca-753cca3dac0d', + 'ofchoodeth:amd', + 'Advanced Micro Devices, Inc.', + 18, + UnderlyingAsset['hoodeth:amd'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + '869113be-3294-4bc5-8d96-a07ffe247b8c', + 'ofchoodeth:baba', + 'Alibaba', + 18, + UnderlyingAsset['hoodeth:baba'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + '1afcbe16-afd1-41ba-b666-a128cbd08d0b', + 'ofchoodeth:googl', + 'Alphabet Inc.', + 18, + UnderlyingAsset['hoodeth:googl'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + 'ae54304e-e45c-4d55-a091-3e53650b043d', + 'ofchoodeth:amzn', + 'Amazon.com, Inc.', + 18, + UnderlyingAsset['hoodeth:amzn'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + '728d1e53-f757-4072-88aa-bb2488192f5a', + 'ofchoodeth:aapl', + 'Apple Inc.', + 18, + UnderlyingAsset['hoodeth:aapl'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + '9788a78e-bea6-4783-972e-d9f5be7a155a', + 'ofchoodeth:apld', + 'Applied Digital', + 18, + UnderlyingAsset['hoodeth:apld'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + '8711bd9d-31e7-4a89-be9e-c2d5b076ee0a', + 'ofchoodeth:amat', + 'Applied Materials', + 18, + UnderlyingAsset['hoodeth:amat'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + '87cf010a-b97b-4372-b7f0-f26d28213969', + 'ofchoodeth:aaoi', + 'Applied Optoelectronics', + 18, + UnderlyingAsset['hoodeth:aaoi'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + '39ea4698-34b7-407c-86e4-0174540f98c1', + 'ofchoodeth:arm', + 'Arm Holdings plc', + 18, + UnderlyingAsset['hoodeth:arm'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + '689843bf-c7af-4fe2-9762-ebbe2f8f1a04', + 'ofchoodeth:asml', + 'ASML Holding NV', + 18, + UnderlyingAsset['hoodeth:asml'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + '76392f2e-7e29-4ea8-bbff-66fa5a933a5f', + 'ofchoodeth:asts', + 'AST SpaceMobile', + 18, + UnderlyingAsset['hoodeth:asts'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + 'ec1fb872-22e4-4b82-a7e7-ce876c4f1316', + 'ofchoodeth:be', + 'Bloom Energy Corporation', + 18, + UnderlyingAsset['hoodeth:be'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + '9d39d53c-f3fc-4749-b233-628bd05ee84b', + 'ofchoodeth:ba', + 'Boeing', + 18, + UnderlyingAsset['hoodeth:ba'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + 'f260d613-b8c8-47a4-a5b6-f0278e0c3c93', + 'ofchoodeth:avgo', + 'Broadcom', + 18, + UnderlyingAsset['hoodeth:avgo'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + 'f9a6462c-a18b-4cb8-a1d0-04b3a0fd644e', + 'ofchoodeth:ccl', + 'Carnival Corporation', + 18, + UnderlyingAsset['hoodeth:ccl'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + '3254e97a-5522-4995-b1a9-07891a282cc4', + 'ofchoodeth:celh', + 'Celsius', + 18, + UnderlyingAsset['hoodeth:celh'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + 'e42f4dbd-4466-4682-8944-a62d26ecc8a9', + 'ofchoodeth:cbrs', + 'Cerebras Systems', + 18, + UnderlyingAsset['hoodeth:cbrs'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + '99f4f2e1-39c7-4422-a751-4e02af80600c', + 'ofchoodeth:crcl', + 'Circle Internet Group Inc', + 18, + UnderlyingAsset['hoodeth:crcl'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + 'fdd7bcb1-455d-43cc-8c1d-265aefdce75b', + 'ofchoodeth:clsk', + 'CleanSpark', + 18, + UnderlyingAsset['hoodeth:clsk'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + '6794aec2-76b3-4a08-966c-626748ffe98d', + 'ofchoodeth:coin', + 'Coinbase', + 18, + UnderlyingAsset['hoodeth:coin'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + '0c879212-b6fa-4ffa-98b1-725afc63784c', + 'ofchoodeth:crwv', + 'CoreWeave', + 18, + UnderlyingAsset['hoodeth:crwv'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + '152d206b-4d86-465e-8e9c-a28a33304cb6', + 'ofchoodeth:glw', + 'Corning', + 18, + UnderlyingAsset['hoodeth:glw'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + '8a90d594-e313-4660-ab44-58a57cb44025', + 'ofchoodeth:cost', + 'Costco', + 18, + UnderlyingAsset['hoodeth:cost'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + '0d450091-4b05-45ea-8515-5c468217f979', + 'ofchoodeth:crwd', + 'CrowdStrike Holdings', + 18, + UnderlyingAsset['hoodeth:crwd'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + 'bff74e88-5447-4feb-a7ce-78ded2f964b4', + 'ofchoodeth:qbts', + 'D-Wave Quantum', + 18, + UnderlyingAsset['hoodeth:qbts'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + '09e7c164-7200-46d8-b5a7-fe4c2628ac24', + 'ofchoodeth:ddog', + 'Datadog', + 18, + UnderlyingAsset['hoodeth:ddog'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + '15dcb80c-b313-4540-86db-1a3883e80f7b', + 'ofchoodeth:dram', + 'Dataram Corporation', + 18, + UnderlyingAsset['hoodeth:dram'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + '44160adf-20cf-4892-acd6-952fc4dfc8f8', + 'ofchoodeth:dell', + 'Dell', + 18, + UnderlyingAsset['hoodeth:dell'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + '5af831db-9401-4e4f-a74b-1e31da809a3c', + 'ofchoodeth:elf', + 'e.l.f. Beauty', + 18, + UnderlyingAsset['hoodeth:elf'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + '414542f5-2434-458b-87d8-3b2aeefa770e', + 'ofchoodeth:sats', + 'EchoStar', + 18, + UnderlyingAsset['hoodeth:sats'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + '29306178-0431-45b0-ad5a-ac09bee8f940', + 'ofchoodeth:lly', + 'Eli Lilly', + 18, + UnderlyingAsset['hoodeth:lly'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + 'ec257f07-f575-4215-8c53-d8b8ee06ed5b', + 'ofchoodeth:p', + 'Everpure', + 18, + UnderlyingAsset['hoodeth:p'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + '96a4b90a-6289-453b-9029-bf78b237bb8f', + 'ofchoodeth:xom', + 'Exxon Mobil', + 18, + UnderlyingAsset['hoodeth:xom'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + '4a7a515c-96db-480c-bf62-314f486db01a', + 'ofchoodeth:flnc', + 'Fluence Energy', + 18, + UnderlyingAsset['hoodeth:flnc'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + '9f9461a8-a51f-4025-a1c3-871930c073e9', + 'ofchoodeth:f', + 'Ford Motor', + 18, + UnderlyingAsset['hoodeth:f'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + '66bb6bdc-f442-4787-bf5e-faef3307b375', + 'ofchoodeth:futu', + 'Futu Holdings', + 18, + UnderlyingAsset['hoodeth:futu'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + '50d09cad-25a8-4c27-b450-f08928823111', + 'ofchoodeth:gme', + 'GameStop', + 18, + UnderlyingAsset['hoodeth:gme'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + '367a6db8-36b9-42ac-a44d-48c2ba1628fe', + 'ofchoodeth:inod', + 'Innodata', + 18, + UnderlyingAsset['hoodeth:inod'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + '3fbefdcc-930a-4795-8ceb-824d54a0c4e2', + 'ofchoodeth:intc', + 'Intel Corporation', + 18, + UnderlyingAsset['hoodeth:intc'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + '87828bfa-61d4-4956-8655-9df105ea8c2f', + 'ofchoodeth:intu', + 'Intuit', + 18, + UnderlyingAsset['hoodeth:intu'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + '63fe81e6-75ae-4069-b64b-c2377217ec07', + 'ofchoodeth:lunr', + 'Intuitive Machines', + 18, + UnderlyingAsset['hoodeth:lunr'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + 'a607c169-9b53-4d91-b4a7-0a4e4c0604df', + 'ofchoodeth:qqq', + 'Invesco QQQ Trust', + 18, + UnderlyingAsset['hoodeth:qqq'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + '559b7236-228d-4ed7-86ac-02f6b2e807e4', + 'ofchoodeth:spmo', + 'Invesco S&P 500 Momentum ETF', + 18, + UnderlyingAsset['hoodeth:spmo'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + 'e02b9e82-2910-4954-b4d3-100615699e26', + 'ofchoodeth:ionq', + 'IonQ', + 18, + UnderlyingAsset['hoodeth:ionq'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + '140ef60f-3a90-4104-939b-074faf73bcce', + 'ofchoodeth:iren', + 'IREN Limited', + 18, + UnderlyingAsset['hoodeth:iren'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + '53cbf6aa-a7e4-42e9-abae-2201a39238e9', + 'ofchoodeth:sgov', + 'iShares 0-3 Month Treasury Bond ETF', + 18, + UnderlyingAsset['hoodeth:sgov'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + '2e99e01c-a271-422d-ade0-8eb7a9538997', + 'ofchoodeth:ewy', + 'iShares MSCI South Korea fund', + 18, + UnderlyingAsset['hoodeth:ewy'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + '7a1da129-1922-4bd0-973f-aeb1261c8236', + 'ofchoodeth:soxx', + 'iShares Semiconductor ETF', + 18, + UnderlyingAsset['hoodeth:soxx'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + '33ba08c6-75bd-4b76-b158-a2b6505ce48b', + 'ofchoodeth:slv', + 'iShares Silver Trust', + 18, + UnderlyingAsset['hoodeth:slv'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + 'f77abccc-5864-4911-96a0-9bc27566a178', + 'ofchoodeth:lulu', + 'Lululemon', + 18, + UnderlyingAsset['hoodeth:lulu'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + '806e2e61-31a8-4c29-aa9d-c6f08c291dc3', + 'ofchoodeth:lite', + 'Lumentum', + 18, + UnderlyingAsset['hoodeth:lite'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + 'd6eb0ad3-4138-4a6b-aadd-6bf36634fbfc', + 'ofchoodeth:mrvl', + 'Marvell Technology', + 18, + UnderlyingAsset['hoodeth:mrvl'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + 'e089470c-2335-4c10-acfe-67698e5d415b', + 'ofchoodeth:mxl', + 'MaxLinear', + 18, + UnderlyingAsset['hoodeth:mxl'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + 'bf014246-5a76-4b2d-a351-c989506eea95', + 'ofchoodeth:meta', + 'Meta Platforms, Inc.', + 18, + UnderlyingAsset['hoodeth:meta'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + 'ae355bf8-423c-4bd7-90ba-fa41664927a9', + 'ofchoodeth:mu', + 'Micron Technology', + 18, + UnderlyingAsset['hoodeth:mu'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + '2610fc79-c281-4624-9975-c2b23f0e4ed1', + 'ofchoodeth:msft', + 'Microsoft Corporation', + 18, + UnderlyingAsset['hoodeth:msft'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + 'fe4033de-fe0a-4a07-a5a5-7e8cb31eb695', + 'ofchoodeth:mdb', + 'MongoDB', + 18, + UnderlyingAsset['hoodeth:mdb'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + '0e371b9a-8bf4-469a-8657-d9b833d1541a', + 'ofchoodeth:nne', + 'Nano Nuclear Energy', + 18, + UnderlyingAsset['hoodeth:nne'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + '1b570888-8891-4984-9643-9a63f377f1c6', + 'ofchoodeth:nvts', + 'Navitas Semiconductor', + 18, + UnderlyingAsset['hoodeth:nvts'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + 'f135e04d-f250-4d47-936d-a01f4977fddf', + 'ofchoodeth:nbis', + 'Nebius Group', + 18, + UnderlyingAsset['hoodeth:nbis'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + 'e37613c1-1de7-4cef-9a97-40b0998f2c35', + 'ofchoodeth:nflx', + 'Netflix', + 18, + UnderlyingAsset['hoodeth:nflx'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + '9b50f9d1-725d-4002-8563-8ad131bcdb60', + 'ofchoodeth:nok', + 'Nokia', + 18, + UnderlyingAsset['hoodeth:nok'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + '06f838b4-f6b3-4c8d-a395-e8a0557b021f', + 'ofchoodeth:nu', + 'Nu', + 18, + UnderlyingAsset['hoodeth:nu'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + '9c927821-eed5-45d6-900e-0d6a119a5f6d', + 'ofchoodeth:nvda', + 'NVIDIA Corporation', + 18, + UnderlyingAsset['hoodeth:nvda'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + '564305d1-da1d-401b-bd56-9203b823e60c', + 'ofchoodeth:orcl', + 'Oracle Corporation', + 18, + UnderlyingAsset['hoodeth:orcl'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + '2c7e60eb-f9dc-48cd-b169-2acd0fef56e5', + 'ofchoodeth:pltr', + 'Palantir Technologies Inc.', + 18, + UnderlyingAsset['hoodeth:pltr'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + '1e4f6f3a-804f-465d-b1f6-e90e5f7bf314', + 'ofchoodeth:peng', + 'Penguin Solutions', + 18, + UnderlyingAsset['hoodeth:peng'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + 'b664491f-4ef3-45ad-90e8-feb8d59d98eb', + 'ofchoodeth:pr', + 'Permian Resources', + 18, + UnderlyingAsset['hoodeth:pr'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + 'd72221d5-3971-42e1-b8bb-1f143f7bfc4f', + 'ofchoodeth:poet', + 'POET Technologies', + 18, + UnderlyingAsset['hoodeth:poet'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + '3161808a-d31f-4f73-b6a7-f8f61d34112c', + 'ofchoodeth:qcom', + 'Qualcomm', + 18, + UnderlyingAsset['hoodeth:qcom'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + 'd178081c-d93a-4fe7-ae7e-25585b968919', + 'ofchoodeth:qubt', + 'Quantum Computing, Inc', + 18, + UnderlyingAsset['hoodeth:qubt'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + '52cae056-b9d9-4033-8d34-5654b76c155b', + 'ofchoodeth:rddt', + 'Reddit', + 18, + UnderlyingAsset['hoodeth:rddt'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + 'ea38b5db-5528-45a3-a9c7-d21b65a3bb89', + 'ofchoodeth:rdw', + 'Redwire', + 18, + UnderlyingAsset['hoodeth:rdw'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + '29e5acf4-05a9-45c9-90fc-922754eeb48a', + 'ofchoodeth:rgti', + 'Rigetti Computing', + 18, + UnderlyingAsset['hoodeth:rgti'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + 'f803588d-4e07-4b16-aa7d-d35c4f1d67aa', + 'ofchoodeth:rivn', + 'Rivian Automotive', + 18, + UnderlyingAsset['hoodeth:rivn'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + 'be82fd79-ba3b-4d1f-b311-6039e0317825', + 'ofchoodeth:rblx', + 'Roblox', + 18, + UnderlyingAsset['hoodeth:rblx'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + '6e09f0ab-1fa0-44fe-889e-33494dab5b61', + 'ofchoodeth:rklb', + 'Rocket Lab Corporation', + 18, + UnderlyingAsset['hoodeth:rklb'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + '06de2415-8bb7-48c9-8453-8ac2da35f591', + 'ofchoodeth:rvi', + 'RVI', + 18, + UnderlyingAsset['hoodeth:rvi'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + '20ab47b6-f825-4ec3-bc9e-1173588676c4', + 'ofchoodeth:sndk', + 'SanDisk', + 18, + UnderlyingAsset['hoodeth:sndk'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + '5c32486f-dbc3-4e0f-a0de-07630f29145b', + 'ofchoodeth:now', + 'ServiceNow', + 18, + UnderlyingAsset['hoodeth:now'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + '0ea0ca53-9ae7-4016-8101-4b09a478c904', + 'ofchoodeth:shop', + 'Shopify', + 18, + UnderlyingAsset['hoodeth:shop'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + '63cecf35-0c22-461e-99f6-2e2b4c3b3fd0', + 'ofchoodeth:sofi', + 'SoFi Technologies', + 18, + UnderlyingAsset['hoodeth:sofi'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + 'c35c8bbc-84c2-4fbd-9371-fe0098bdb49c', + 'ofchoodeth:spy', + 'SPDR S&P 500 ETF Trust', + 18, + UnderlyingAsset['hoodeth:spy'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + '832e7edc-8ba9-4e7f-a865-98570bb71331', + 'ofchoodeth:xlk', + 'State Street Technology Select Sector SPDR ETF', + 18, + UnderlyingAsset['hoodeth:xlk'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + '6b9fec79-284e-4b76-9276-12a9d9ba997c', + 'ofchoodeth:mstr', + 'Strategy Inc.', + 18, + UnderlyingAsset['hoodeth:mstr'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + 'eb20c03f-cedb-4585-a952-6d90210b139e', + 'ofchoodeth:smci', + 'Super Micro Computer', + 18, + UnderlyingAsset['hoodeth:smci'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + '5a57aa8e-31f2-4cf8-88ee-cf74ba3d7298', + 'ofchoodeth:tsm', + 'Taiwan Semiconductor Manufacturing', + 18, + UnderlyingAsset['hoodeth:tsm'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + 'e58b5516-cfae-4417-b4ca-2ffb45b4a160', + 'ofchoodeth:ttwo', + 'Take-Two Interactive Software', + 18, + UnderlyingAsset['hoodeth:ttwo'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + 'a85e99d4-d6bf-4b08-b659-ea363197d4e4', + 'ofchoodeth:nasa', + 'Tema Space Innovators ETF', + 18, + UnderlyingAsset['hoodeth:nasa'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + '1806a1c6-56b1-4107-89af-8c93159905b9', + 'ofchoodeth:tsla', + 'Tesla', + 18, + UnderlyingAsset['hoodeth:tsla'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + '1d4ad8ac-a6fa-4825-9f3f-0c5c9d4c6c47', + 'ofchoodeth:tsem', + 'Tower Semiconductor', + 18, + UnderlyingAsset['hoodeth:tsem'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + 'a8282f20-6734-4df6-91e1-859f34288e3b', + 'ofchoodeth:umc', + 'United Microelectronics', + 18, + UnderlyingAsset['hoodeth:umc'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + '7ca62551-0f32-47c2-9e35-a55f5a782ed6', + 'ofchoodeth:uso', + 'United States Oil Fund, LP', + 18, + UnderlyingAsset['hoodeth:uso'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + 'af4c4cea-d9a9-4f75-807c-22c7e4a0ecc7', + 'ofchoodeth:ups', + 'UPS', + 18, + UnderlyingAsset['hoodeth:ups'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + 'd1f75f57-9f62-4c30-bfd9-8ed76884f884', + 'ofchoodeth:usar', + 'USA Rare Earth Inc.', + 18, + UnderlyingAsset['hoodeth:usar'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + '4099ae4f-a6d0-4ee5-b26b-029cdd93645a', + 'ofchoodeth:usdg', + 'USDG', + 18, + UnderlyingAsset['hoodeth:usdg'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + '43fb356f-8b1e-4569-b6b8-982f8bfb0465', + 'ofchoodeth:week', + 'Weekly T-Bill ETF', + 18, + UnderlyingAsset['hoodeth:week'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + '0e82cc04-8f20-4623-b1ac-7c9e90b5d5d0', + 'ofchoodeth:wday', + 'Workday', + 18, + UnderlyingAsset['hoodeth:wday'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + '755abf18-75ff-4f43-9500-6f5e33089210', + 'ofchoodeth:xndu', + 'Xanadu Quantum', + 18, + UnderlyingAsset['hoodeth:xndu'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + '1448c582-1250-490f-9c3c-476442ac55ca', + 'ofchoodeth:zm', + 'Zoom', + 18, + UnderlyingAsset['hoodeth:zm'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + ofcerc20( + 'a92cd45a-5dff-409a-a5ab-167927318021', + 'ofchoodeth:zs', + 'Zscaler', + 18, + UnderlyingAsset['hoodeth:zs'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hoodeth' + ), + tofcerc20( + 'df59331f-b380-4fb7-ad9a-0e0c0c3ec730', + 'ofcthoodeth:amzn', + 'Amazon', + 18, + UnderlyingAsset['thoodeth:amzn'], + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + 'thoodeth' + ), + tofcerc20( + '6abb714a-de88-4181-988e-86fbbfbc9c2e', + 'ofcthoodeth:tsla', + 'Tesla', + 18, + UnderlyingAsset['thoodeth:tsla'], + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + 'thoodeth' + ), +]; From 87d04a789b5e0bf44ad7d6b64e860873237f4c30 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Fri, 5 Jun 2026 18:57:45 +0000 Subject: [PATCH 04/34] fix: fixes for external signer callback Ticket: WCN-685 --- modules/sdk-core/src/bitgo/wallet/iWallets.ts | 6 +- modules/sdk-core/src/bitgo/wallet/wallets.ts | 43 ++++-- .../bitgo/wallet/walletsExternalSigner.ts | 127 ++++++++++++++++-- 3 files changed, 152 insertions(+), 24 deletions(-) diff --git a/modules/sdk-core/src/bitgo/wallet/iWallets.ts b/modules/sdk-core/src/bitgo/wallet/iWallets.ts index 5aecb03edc..aee366b40b 100644 --- a/modules/sdk-core/src/bitgo/wallet/iWallets.ts +++ b/modules/sdk-core/src/bitgo/wallet/iWallets.ts @@ -70,8 +70,8 @@ export interface CreateKeychainCallbackParams { export interface CreateKeychainCallbackResult { pub: string; - type: string; - source: string; + type: 'independent'; + source: 'user' | 'backup'; } export type CreateKeychainCallback = (params: CreateKeychainCallbackParams) => Promise; @@ -116,6 +116,8 @@ export interface GenerateWalletWithExternalSignerOptions extends Omit { label: string; createKeychainCallback: CreateKeychainCallback; + /** Optional user-key signatures over backup/bitgo pubs. Omit when the external signer cannot produce them (equivalent to a cold wallet). */ + keySignatures?: { backup: string; bitgo: string }; } export const GenerateLightningWalletOptionsCodec = t.intersection( diff --git a/modules/sdk-core/src/bitgo/wallet/wallets.ts b/modules/sdk-core/src/bitgo/wallet/wallets.ts index 9d1c12c899..4f7faf4551 100644 --- a/modules/sdk-core/src/bitgo/wallet/wallets.ts +++ b/modules/sdk-core/src/bitgo/wallet/wallets.ts @@ -12,7 +12,7 @@ import * as common from '../../common'; import { IBaseCoin, KeychainsTriplet, SupplementGenerateWalletOptions } from '../baseCoin'; import { BitGoBase } from '../bitgoBase'; import { getSharedSecret } from '../ecdh'; -import { AddKeychainOptions, Keychain, KeyIndices, KeyType } from '../keychain'; +import { AddKeychainOptions, Keychain, KeyIndices } from '../keychain'; import { decodeOrElse, promiseProps, RequestTracer } from '../utils'; import { AcceptShareOptions, @@ -743,8 +743,9 @@ export class Wallets implements IWallets { throw new Error('external signer wallet generation is only supported for onchain multisig wallets'); } - const conflictingParams = ['passphrase', 'userKey', 'backupXpub', 'backupXpubProvider'] as const; - for (const key of conflictingParams) { + // these belong to the passphrase-based path and are incompatible with createKeychainCallback + const passphrasePathParams = ['passphrase', 'userKey', 'backupXpub', 'backupXpubProvider'] as const; + for (const key of passphrasePathParams) { if (!_.isUndefined(params[key])) { throw new Error(`createKeychainCallback cannot be used with ${key}`); } @@ -833,16 +834,30 @@ export class Wallets implements IWallets { const coin = this.baseCoin.getChain(); const createAndUploadKeychain = async (source: 'user' | 'backup'): Promise => { - const keychainFromCallback = await createKeychainCallback({ source, coin }); - if (keychainFromCallback.source !== source) { - throw new Error(`createKeychainCallback returned source ${keychainFromCallback.source}, expected ${source}`); + try { + const keychainFromCallback = await createKeychainCallback({ source, coin }); + if (keychainFromCallback.source !== source) { + throw new Error(`createKeychainCallback returned source ${keychainFromCallback.source}, expected ${source}`); + } + if (keychainFromCallback.type !== 'independent') { + throw new Error( + `createKeychainCallback returned invalid type ${keychainFromCallback.type}, expected 'independent' for onchain multisig` + ); + } + if (!this.baseCoin.isValidPub(keychainFromCallback.pub)) { + throw new Error(`createKeychainCallback returned invalid pub for ${source} key on ${coin}`); + } + return this.baseCoin.keychains().add({ + pub: keychainFromCallback.pub, + keyType: keychainFromCallback.type, + source: keychainFromCallback.source, + reqId, + }); + } catch (error) { + throw new Error( + `Failed to create ${source} keychain: ${error instanceof Error ? error.message : String(error)}` + ); } - return this.baseCoin.keychains().add({ - pub: keychainFromCallback.pub, - keyType: keychainFromCallback.type as KeyType, - source: keychainFromCallback.source, - reqId, - }); }; const { userKeychain, backupKeychain, bitgoKeychain }: KeychainsTriplet = await promiseProps({ @@ -855,6 +870,10 @@ export class Wallets implements IWallets { walletParams.keys = [userKeychain.id, backupKeychain.id, bitgoKeychain.id]; + if (params.keySignatures) { + walletParams.keySignatures = params.keySignatures; + } + const keychains = { userKeychain, backupKeychain, diff --git a/modules/sdk-core/test/unit/bitgo/wallet/walletsExternalSigner.ts b/modules/sdk-core/test/unit/bitgo/wallet/walletsExternalSigner.ts index cae275df70..428a197918 100644 --- a/modules/sdk-core/test/unit/bitgo/wallet/walletsExternalSigner.ts +++ b/modules/sdk-core/test/unit/bitgo/wallet/walletsExternalSigner.ts @@ -4,6 +4,7 @@ import 'should'; import { Wallets } from '../../../../src/bitgo/wallet/wallets'; import { CreateKeychainCallback } from '../../../../src/bitgo/wallet/iWallets'; +import { Wallet } from '../../../../src/bitgo/wallet/wallet'; describe('Wallets - external signer onchain wallet generation', function () { let wallets: Wallets; @@ -24,12 +25,12 @@ describe('Wallets - external signer onchain wallet generation', function () { createKeychainCallback = sinon.stub(); createKeychainCallback.withArgs({ source: 'user', coin: 'tbtc' }).resolves({ pub: userPub, - type: 'tbtc', + type: 'independent', source: 'user', }); createKeychainCallback.withArgs({ source: 'backup', coin: 'tbtc' }).resolves({ pub: backupPub, - type: 'tbtc', + type: 'independent', source: 'backup', }); @@ -62,6 +63,7 @@ describe('Wallets - external signer onchain wallet generation', function () { url: sinon.stub().returns('/api/v2/tbtc/wallet/add'), getConfig: sinon.stub().returns({ features: [] }), supplementGenerateWallet: sinon.stub().callsFake((walletParams: unknown) => Promise.resolve(walletParams)), + isValidPub: sinon.stub().returns(true), }; wallets = new Wallets(mockBitGo, mockBaseCoin); @@ -86,7 +88,7 @@ describe('Wallets - external signer onchain wallet generation', function () { const addUserParams = mockKeychains.add.getCall(0).args[0]; addUserParams.should.have.property('pub', userPub); - addUserParams.should.have.property('keyType', 'tbtc'); + addUserParams.should.have.property('keyType', 'independent'); addUserParams.should.have.property('source', 'user'); const walletBody = sendStub.firstCall.args[0]; @@ -103,7 +105,7 @@ describe('Wallets - external signer onchain wallet generation', function () { it('should reject when callback source does not match requested source', async function () { createKeychainCallback.withArgs({ source: 'user', coin: 'tbtc' }).resolves({ pub: userPub, - type: 'tbtc', + type: 'independent', source: 'backup', }); @@ -112,7 +114,32 @@ describe('Wallets - external signer onchain wallet generation', function () { label: 'External Signer Wallet', createKeychainCallback, }) - .should.be.rejectedWith('createKeychainCallback returned source backup, expected user'); + .should.be.rejectedWith( + 'Failed to create user keychain: createKeychainCallback returned source backup, expected user' + ); + }); + + it('should reject invalid type from callback', async function () { + const badTypeCallback = sinon.stub(); + badTypeCallback.withArgs({ source: 'user', coin: 'tbtc' }).resolves({ + pub: userPub, + type: 'tss', + source: 'user', + }); + badTypeCallback.withArgs({ source: 'backup', coin: 'tbtc' }).resolves({ + pub: backupPub, + type: 'independent', + source: 'backup', + }); + + await wallets + .generateWalletWithExternalSigner({ + label: 'External Signer Wallet', + createKeychainCallback: badTypeCallback, + }) + .should.be.rejectedWith( + "Failed to create user keychain: createKeychainCallback returned invalid type tss, expected 'independent' for onchain multisig" + ); }); it('should reject TSS multisig type', async function () { @@ -149,7 +176,7 @@ describe('Wallets - external signer onchain wallet generation', function () { await wallets .generateWalletWithExternalSigner({ label: 'Wallet', - webauthnInfo: { otpDeviceId: 'dev-id', prfSalt: 'salt', passphrase: 'pass' } as any, + webauthnInfo: { otpDeviceId: 'dev-id', prfSalt: 'salt', passphrase: 'pass' }, createKeychainCallback, }) .should.be.rejectedWith('webauthnInfo is not supported for external signer wallet generation'); @@ -177,16 +204,96 @@ describe('Wallets - external signer onchain wallet generation', function () { }) .should.be.rejectedWith('distributed custody wallets must be type: cold'); }); + + it('should reject when callback returns invalid pub for coin', async function () { + // only invalidate user pub so the rejection source is deterministic + mockBaseCoin.isValidPub.callsFake((pub: string) => pub !== userPub); + + await wallets + .generateWalletWithExternalSigner({ + label: 'External Signer Wallet', + createKeychainCallback, + }) + .should.be.rejectedWith( + 'Failed to create user keychain: createKeychainCallback returned invalid pub for user key on tbtc' + ); + }); + + it('should wrap error when callback throws', async function () { + createKeychainCallback.withArgs({ source: 'user', coin: 'tbtc' }).rejects(new Error('HSM unreachable')); + + await wallets + .generateWalletWithExternalSigner({ + label: 'External Signer Wallet', + createKeychainCallback, + }) + .should.be.rejectedWith('Failed to create user keychain: HSM unreachable'); + }); + + it('should reject with backup keychain error when backup callback throws', async function () { + createKeychainCallback.withArgs({ source: 'backup', coin: 'tbtc' }).rejects(new Error('HSM unreachable')); + + await wallets + .generateWalletWithExternalSigner({ + label: 'External Signer Wallet', + createKeychainCallback, + }) + .should.be.rejectedWith('Failed to create backup keychain: HSM unreachable'); + + assert.strictEqual(mockBitGo.post.callCount, 0); + }); + + it('should wrap non-Error thrown by callback', async function () { + createKeychainCallback + .withArgs({ source: 'user', coin: 'tbtc' }) + .returns(Promise.reject('plain string rejection')); + + await wallets + .generateWalletWithExternalSigner({ + label: 'External Signer Wallet', + createKeychainCallback, + }) + .should.be.rejectedWith('Failed to create user keychain: plain string rejection'); + }); + + it('should forward keySignatures to wallet params when provided', async function () { + const keySignatures = { + backup: 'deadbeef01', + bitgo: 'deadbeef02', + }; + + await wallets.generateWalletWithExternalSigner({ + label: 'External Signer Wallet', + enterprise: 'enterprise-id', + createKeychainCallback, + keySignatures, + }); + + const walletBody = sendStub.firstCall.args[0]; + walletBody.should.have.property('keySignatures'); + walletBody.keySignatures.should.deepEqual(keySignatures); + }); + + it('should not include keySignatures in wallet params when not provided', async function () { + await wallets.generateWalletWithExternalSigner({ + label: 'External Signer Wallet', + enterprise: 'enterprise-id', + createKeychainCallback, + }); + + const walletBody = sendStub.firstCall.args[0]; + walletBody.should.not.have.property('keySignatures'); + }); }); describe('generateWallet with createKeychainCallback', function () { it('should delegate to generateWalletWithExternalSigner', async function () { const generateWalletWithExternalSignerStub = sinon.stub(wallets, 'generateWalletWithExternalSigner').resolves({ responseType: 'WalletWithKeychains', - wallet: {} as any, - userKeychain: { id: 'user-key-id', pub: userPub, type: 'independent' } as any, - backupKeychain: { id: 'backup-key-id', pub: backupPub, type: 'independent' } as any, - bitgoKeychain: { id: 'bitgo-key-id', pub: bitgoPub, type: 'independent' } as any, + wallet: sinon.createStubInstance(Wallet), + userKeychain: { id: 'user-key-id', pub: userPub, type: 'independent' as const }, + backupKeychain: { id: 'backup-key-id', pub: backupPub, type: 'independent' as const }, + bitgoKeychain: { id: 'bitgo-key-id', pub: bitgoPub, type: 'independent' as const }, }); await wallets.generateWallet({ From e30261d7b52f16de40ea1775206c9b1891db8273 Mon Sep 17 00:00:00 2001 From: Gokul Krishnaa Devaraju Date: Tue, 9 Jun 2026 10:59:29 -0700 Subject: [PATCH 05/34] fix(scripts): retry yarn audit up to 3x on transient network errors WCN-865 Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/npmjs-release.yml | 8 ++++++-- .github/workflows/publish.yml | 7 ++++++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/.github/workflows/npmjs-release.yml b/.github/workflows/npmjs-release.yml index 59054057f1..23fa911d9b 100644 --- a/.github/workflows/npmjs-release.yml +++ b/.github/workflows/npmjs-release.yml @@ -236,9 +236,13 @@ jobs: run: | yarn install --frozen-lockfile + - name: Install retry + uses: BitGo/install-github-release-binary@v2 + with: + targets: EricCrosson/retry@v1 + - name: Run yarn audit - run: | - yarn run audit-high + run: retry --up-to 3x -- yarn run audit-high - name: Run dependency check run: | diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index f6de2eeeac..56d03a1235 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -36,8 +36,13 @@ jobs: - name: Install BitGoJS run: sfw yarn install --with-frozen-lockfile + - name: Install retry + uses: BitGo/install-github-release-binary@v2 + with: + targets: EricCrosson/retry@v1 + - name: Audit Dependencies - run: yarn run improved-yarn-audit --min-severity high + run: retry --up-to 3x -- yarn run improved-yarn-audit --min-severity high - name: Set Environment Variable for Alpha if: github.ref != 'refs/heads/master' # only publish changes if on feature branches From 49433e0cc1fd226565f8a8080984f8b71e358b0b Mon Sep 17 00:00:00 2001 From: Gokul Krishnaa Devaraju Date: Tue, 9 Jun 2026 11:13:43 -0700 Subject: [PATCH 06/34] fix(scripts): use built-in retry-on-network-failure and 3s backoff WCN-865 Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/npmjs-release.yml | 2 +- .github/workflows/publish.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/npmjs-release.yml b/.github/workflows/npmjs-release.yml index 23fa911d9b..49f740d471 100644 --- a/.github/workflows/npmjs-release.yml +++ b/.github/workflows/npmjs-release.yml @@ -242,7 +242,7 @@ jobs: targets: EricCrosson/retry@v1 - name: Run yarn audit - run: retry --up-to 3x -- yarn run audit-high + run: retry --up-to 2x --every 3s -- yarn run audit-high --retry-on-network-failure - name: Run dependency check run: | diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 56d03a1235..85b7f58459 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -42,7 +42,7 @@ jobs: targets: EricCrosson/retry@v1 - name: Audit Dependencies - run: retry --up-to 3x -- yarn run improved-yarn-audit --min-severity high + run: retry --up-to 2x --every 3s -- yarn run improved-yarn-audit --min-severity high --retry-on-network-failure - name: Set Environment Variable for Alpha if: github.ref != 'refs/heads/master' # only publish changes if on feature branches From 326cc6e2fa784b0d61dbea7cb2c2bdfbdad22cea Mon Sep 17 00:00:00 2001 From: Gokul Krishnaa Devaraju Date: Tue, 9 Jun 2026 15:29:16 -0700 Subject: [PATCH 07/34] fix(scripts): pin EricCrosson/retry to v1.4.8 with SHA256 digest Pins the retry binary to an exact version and checksum to address supply-chain security concerns in the npm publish pipeline. WCN-865 Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/npmjs-release.yml | 2 +- .github/workflows/publish.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/npmjs-release.yml b/.github/workflows/npmjs-release.yml index 49f740d471..ab3ef1e1c6 100644 --- a/.github/workflows/npmjs-release.yml +++ b/.github/workflows/npmjs-release.yml @@ -239,7 +239,7 @@ jobs: - name: Install retry uses: BitGo/install-github-release-binary@v2 with: - targets: EricCrosson/retry@v1 + targets: EricCrosson/retry@v1.4.8:sha256-15224553f40d5d16dcc1a696798741227c79670a41f43e522002e634aa1d7c64 - name: Run yarn audit run: retry --up-to 2x --every 3s -- yarn run audit-high --retry-on-network-failure diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 85b7f58459..f7239ea240 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -39,7 +39,7 @@ jobs: - name: Install retry uses: BitGo/install-github-release-binary@v2 with: - targets: EricCrosson/retry@v1 + targets: EricCrosson/retry@v1.4.8:sha256-15224553f40d5d16dcc1a696798741227c79670a41f43e522002e634aa1d7c64 - name: Audit Dependencies run: retry --up-to 2x --every 3s -- yarn run improved-yarn-audit --min-severity high --retry-on-network-failure From 95e1e8408001179275f6e9a0592fae496f306fa0 Mon Sep 17 00:00:00 2001 From: Shubham Damkondwar Date: Wed, 10 Jun 2026 08:58:44 +0530 Subject: [PATCH 08/34] feat(sdk-coin-starknet): implement deploy account transaction support CECHO-927 TICKET: CECHO-927 --- .../sdk-coin-starknet/src/lib/constants.ts | 3 + modules/sdk-coin-starknet/src/lib/iface.ts | 20 +++ modules/sdk-coin-starknet/src/lib/index.ts | 1 + .../sdk-coin-starknet/src/lib/transaction.ts | 40 ++++- .../src/lib/transactionBuilderFactory.ts | 9 +- modules/sdk-coin-starknet/src/lib/utils.ts | 71 ++++++++- .../src/lib/walletInitializationBuilder.ts | 130 ++++++++++++++++ modules/sdk-coin-starknet/test/unit/utils.ts | 43 ++++++ .../test/unit/walletInitializationBuilder.ts | 146 ++++++++++++++++++ 9 files changed, 456 insertions(+), 7 deletions(-) create mode 100644 modules/sdk-coin-starknet/src/lib/walletInitializationBuilder.ts create mode 100644 modules/sdk-coin-starknet/test/unit/walletInitializationBuilder.ts diff --git a/modules/sdk-coin-starknet/src/lib/constants.ts b/modules/sdk-coin-starknet/src/lib/constants.ts index 877b83c45a..398cdd249f 100644 --- a/modules/sdk-coin-starknet/src/lib/constants.ts +++ b/modules/sdk-coin-starknet/src/lib/constants.ts @@ -23,6 +23,9 @@ export const DEFAULT_SEED_SIZE_BYTES = 16; // V3 transaction hash prefix: encodeShortString("invoke") export const INVOKE_TX_PREFIX = 0x696e766f6b65n; +// V3 transaction hash prefix: encodeShortString("deploy_account") +export const DEPLOY_ACCOUNT_TX_PREFIX = 0x6465706c6f795f6163636f756e74n; + // V3 transaction version export const TRANSACTION_VERSION_3 = 3n; diff --git a/modules/sdk-coin-starknet/src/lib/iface.ts b/modules/sdk-coin-starknet/src/lib/iface.ts index 98c8746218..3614a410ae 100644 --- a/modules/sdk-coin-starknet/src/lib/iface.ts +++ b/modules/sdk-coin-starknet/src/lib/iface.ts @@ -34,6 +34,12 @@ export interface StarknetTransactionData { nonceDataAvailabilityMode?: number; feeDataAvailabilityMode?: number; compiledCalldata?: string[]; + /** DEPLOY_ACCOUNT: OZ EthAccount class hash. */ + classHash?: string; + /** DEPLOY_ACCOUNT: constructor calldata (pubkey limbs). */ + constructorCalldata?: string[]; + /** DEPLOY_ACCOUNT: address salt derived from pubkey. */ + contractAddressSalt?: string; } export interface InvokeTransactionHashParams { @@ -50,6 +56,20 @@ export interface InvokeTransactionHashParams { proofFacts?: string[]; } +export interface DeployAccountTransactionHashParams { + contractAddress: string; + classHash: string; + constructorCalldata: string[]; + contractAddressSalt: string; + chainId: string; + nonce: string; + resourceBounds: StarknetResourceBounds; + tip?: string; + nonceDataAvailabilityMode?: number; + feeDataAvailabilityMode?: number; + paymasterData?: string[]; +} + export interface ParsedTransferData { recipient: string; amount: string; diff --git a/modules/sdk-coin-starknet/src/lib/index.ts b/modules/sdk-coin-starknet/src/lib/index.ts index 839446d4bd..047e8281a1 100644 --- a/modules/sdk-coin-starknet/src/lib/index.ts +++ b/modules/sdk-coin-starknet/src/lib/index.ts @@ -4,6 +4,7 @@ export * from './iface'; export { KeyPair } from './keyPair'; export { TransactionBuilder } from './transactionBuilder'; export { TransferBuilder } from './transferBuilder'; +export { WalletInitializationBuilder } from './walletInitializationBuilder'; export { TransactionBuilderFactory } from './transactionBuilderFactory'; export { Transaction } from './transaction'; export { Utils }; diff --git a/modules/sdk-coin-starknet/src/lib/transaction.ts b/modules/sdk-coin-starknet/src/lib/transaction.ts index d18ee28a55..95466f8c45 100644 --- a/modules/sdk-coin-starknet/src/lib/transaction.ts +++ b/modules/sdk-coin-starknet/src/lib/transaction.ts @@ -80,6 +80,9 @@ export class Transaction extends BaseTransaction { compiledCalldata: parsed.compiledCalldata, nonceDataAvailabilityMode: parsed.nonceDataAvailabilityMode, feeDataAvailabilityMode: parsed.feeDataAvailabilityMode, + classHash: parsed.classHash, + constructorCalldata: parsed.constructorCalldata, + contractAddressSalt: parsed.contractAddressSalt, }; if (parsed.signature && parsed.signature.length > 0) { @@ -146,13 +149,44 @@ export class Transaction extends BaseTransaction { return Buffer.from(JSON.stringify(data), 'utf-8').toString('hex'); } - /** @inheritdoc — returns Starknet RPC-ready JSON string for starknet_addInvokeTransaction. */ + /** @inheritdoc — returns Starknet RPC-ready JSON for addInvoke or addDeployAccount. */ toBroadcastFormat(): string { const data = this._starknetTransactionData; if (!data) { throw new InvalidTransactionError('Empty transaction'); } - return JSON.stringify({ + + const payload = + data.transactionType === StarknetTransactionType.DEPLOY_ACCOUNT + ? this.buildDeployAccountPayload(data) + : this.buildInvokePayload(data); + + return JSON.stringify(payload); + } + private buildDeployAccountPayload(data: StarknetTransactionData) { + if (!data.classHash || !data.constructorCalldata || !data.contractAddressSalt) { + throw new InvalidTransactionError('Incomplete deploy account transaction'); + } + + return { + type: 'DEPLOY_ACCOUNT', + version: '0x3', + signature: data.signature || [], + nonce: data.nonce, + contract_address_salt: data.contractAddressSalt, + constructor_calldata: data.constructorCalldata, + class_hash: data.classHash, + sender_address: data.senderAddress, + resource_bounds: resolveResourceBounds(data), + tip: data.tip || '0x0', + paymaster_data: [], + nonce_data_availability_mode: 'L1', + fee_data_availability_mode: 'L1', + }; + } + + private buildInvokePayload(data: StarknetTransactionData) { + return { type: 'INVOKE', version: '0x3', sender_address: data.senderAddress, @@ -165,7 +199,7 @@ export class Transaction extends BaseTransaction { account_deployment_data: [], nonce_data_availability_mode: 'L1', fee_data_availability_mode: 'L1', - }); + }; } /** @inheritdoc */ diff --git a/modules/sdk-coin-starknet/src/lib/transactionBuilderFactory.ts b/modules/sdk-coin-starknet/src/lib/transactionBuilderFactory.ts index de691811d8..e1916cff4f 100644 --- a/modules/sdk-coin-starknet/src/lib/transactionBuilderFactory.ts +++ b/modules/sdk-coin-starknet/src/lib/transactionBuilderFactory.ts @@ -1,8 +1,9 @@ -import { BaseTransactionBuilderFactory, InvalidTransactionError, MethodNotImplementedError } from '@bitgo/sdk-core'; +import { BaseTransactionBuilderFactory, InvalidTransactionError } from '@bitgo/sdk-core'; import { BaseCoin as CoinConfig } from '@bitgo/statics'; import { Transaction } from './transaction'; import { TransactionBuilder } from './transactionBuilder'; import { TransferBuilder } from './transferBuilder'; +import { WalletInitializationBuilder } from './walletInitializationBuilder'; import { StarknetTransactionType } from './iface'; export class TransactionBuilderFactory extends BaseTransactionBuilderFactory { @@ -18,6 +19,8 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory { switch (transaction.starknetTransactionData.transactionType) { case StarknetTransactionType.INVOKE: return this.getTransferBuilder(transaction); + case StarknetTransactionType.DEPLOY_ACCOUNT: + return this.getWalletInitializationBuilder(transaction); default: throw new InvalidTransactionError('Invalid transaction type'); } @@ -39,7 +42,7 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory { } /** @inheritdoc */ - getWalletInitializationBuilder(): void { - throw new MethodNotImplementedError(); + getWalletInitializationBuilder(tx?: Transaction): WalletInitializationBuilder { + return TransactionBuilderFactory.initializeBuilder(tx, new WalletInitializationBuilder(this._coinConfig)); } } diff --git a/modules/sdk-coin-starknet/src/lib/utils.ts b/modules/sdk-coin-starknet/src/lib/utils.ts index 6883bfc9d8..9c78750abd 100644 --- a/modules/sdk-coin-starknet/src/lib/utils.ts +++ b/modules/sdk-coin-starknet/src/lib/utils.ts @@ -6,12 +6,20 @@ import { ADDR_BOUND, CONTRACT_ADDRESS_PREFIX, INVOKE_TX_PREFIX, + DEPLOY_ACCOUNT_TX_PREFIX, TRANSACTION_VERSION_3, L1_GAS_NAME, L2_GAS_NAME, L1_DATA_GAS_NAME, } from './constants'; -import { StarknetTransactionData, StarknetCall, ParsedTransferData, InvokeTransactionHashParams } from './iface'; +import { + StarknetTransactionData, + StarknetTransactionType, + StarknetCall, + ParsedTransferData, + InvokeTransactionHashParams, + DeployAccountTransactionHashParams, +} from './iface'; import { ecc } from '@bitgo/secp256k1'; /** @@ -207,6 +215,17 @@ export function validateRawTransaction(tx: StarknetTransactionData): void { if (!isValidAddress(tx.senderAddress)) { throw new Error(`Invalid sender address: ${tx.senderAddress}`); } + if (tx.transactionType === StarknetTransactionType.DEPLOY_ACCOUNT) { + if (!tx.classHash) { + throw new Error('Missing class hash for deploy account transaction'); + } + if (!tx.constructorCalldata || tx.constructorCalldata.length === 0) { + throw new Error('Missing constructor calldata for deploy account transaction'); + } + if (!tx.contractAddressSalt) { + throw new Error('Missing contract address salt for deploy account transaction'); + } + } } /** @@ -309,6 +328,55 @@ export function calculateInvokeTransactionHash(params: InvokeTransactionHashPara return '0x' + hash.toString(16); } +/** + * Compute the Poseidon V3 DEPLOY_ACCOUNT transaction hash per SNIP-8 (starknet.js v3). + */ +export function calculateDeployAccountTransactionHash(params: DeployAccountTransactionHashParams): string { + const { + contractAddress, + classHash, + constructorCalldata, + contractAddressSalt, + chainId, + nonce, + resourceBounds, + tip = '0x0', + nonceDataAvailabilityMode = 0, + feeDataAvailabilityMode = 0, + paymasterData = [], + } = params; + + const feeFieldHash = poseidonHashMany([ + BigInt(tip), + encodeResourceBound(L1_GAS_NAME, resourceBounds.l1_gas.max_amount, resourceBounds.l1_gas.max_price_per_unit), + encodeResourceBound(L2_GAS_NAME, resourceBounds.l2_gas.max_amount, resourceBounds.l2_gas.max_price_per_unit), + encodeResourceBound( + L1_DATA_GAS_NAME, + resourceBounds.l1_data_gas.max_amount, + resourceBounds.l1_data_gas.max_price_per_unit + ), + ]); + + const daMode = (BigInt(nonceDataAvailabilityMode) << 32n) | BigInt(feeDataAvailabilityMode); + + const hashFields: bigint[] = [ + DEPLOY_ACCOUNT_TX_PREFIX, + TRANSACTION_VERSION_3, + BigInt(contractAddress), + feeFieldHash, + poseidonHashMany(paymasterData.map(BigInt)), + BigInt(chainId), + BigInt(nonce), + daMode, + poseidonHashMany(constructorCalldata.map(BigInt)), + BigInt(classHash), + BigInt(contractAddressSalt), + ]; + + const hash = poseidonHashMany(hashFields); + return '0x' + hash.toString(16); +} + export default { isValidAddress, isValidPublicKey, @@ -327,4 +395,5 @@ export default { getSelectorFromName, compileExecuteCalldata, calculateInvokeTransactionHash, + calculateDeployAccountTransactionHash, }; diff --git a/modules/sdk-coin-starknet/src/lib/walletInitializationBuilder.ts b/modules/sdk-coin-starknet/src/lib/walletInitializationBuilder.ts new file mode 100644 index 0000000000..85555aa0b9 --- /dev/null +++ b/modules/sdk-coin-starknet/src/lib/walletInitializationBuilder.ts @@ -0,0 +1,130 @@ +import { BuildTransactionError } from '@bitgo/sdk-core'; +import { BaseCoin as CoinConfig } from '@bitgo/statics'; +import { TransactionBuilder } from './transactionBuilder'; +import { Transaction } from './transaction'; +import { StarknetTransactionData, StarknetTransactionType } from './iface'; +import { OZ_ETH_ACCOUNT_CLASS_HASH } from './constants'; +import utils from './utils'; + +/** + * Builds DEPLOY_ACCOUNT v3 transactions for counterfactual EthAccount activation. + */ +export class WalletInitializationBuilder extends TransactionBuilder { + protected _classHash: string = OZ_ETH_ACCOUNT_CLASS_HASH; + protected _constructorCalldata?: string[]; + protected _contractAddressSalt?: string; + + constructor(_coinConfig: Readonly) { + super(_coinConfig); + } + + protected get transactionType(): StarknetTransactionType { + return StarknetTransactionType.DEPLOY_ACCOUNT; + } + + /** + * Set deploy parameters from a secp256k1 public key (compressed or uncompressed). + * Derives counterfactual address, constructor calldata, and salt. + */ + public fromPublicKey(pubKey: string): this { + if (!utils.isValidPublicKey(pubKey)) { + throw new BuildTransactionError('Invalid pubKey, got: ' + pubKey); + } + const fullPublicKey = utils.getUncompressedPublicKey(pubKey); + const { address, constructorCalldata, salt } = utils.computeStarknetAddress(fullPublicKey); + this._constructorCalldata = constructorCalldata; + this._contractAddressSalt = salt; + return this.sender(address, pubKey); + } + + public classHash(classHash: string): this { + if (!classHash || !utils.isValidAddress(classHash)) { + throw new BuildTransactionError('Invalid class hash, got: ' + classHash); + } + this._classHash = classHash; + return this; + } + + /** @inheritdoc */ + initBuilder(tx: Transaction): void { + super.initBuilder(tx); + const data = tx.starknetTransactionData; + if (data.classHash) { + this._classHash = data.classHash; + } + if (data.constructorCalldata) { + this._constructorCalldata = data.constructorCalldata; + } + if (data.contractAddressSalt) { + this._contractAddressSalt = data.contractAddressSalt; + } + } + + /** @inheritdoc */ + protected async buildImplementation(): Promise { + this.ensureDeployFields(); + + const contractAddress = this._sender as string; + const chainId = this._chainId as string; + const nonce = this._nonce as string; + const constructorCalldata = this._constructorCalldata as string[]; + const contractAddressSalt = this._contractAddressSalt as string; + + const transactionHash = utils.calculateDeployAccountTransactionHash({ + contractAddress, + classHash: this._classHash, + constructorCalldata, + contractAddressSalt, + chainId, + nonce, + resourceBounds: this._resourceBounds, + tip: this._tip, + }); + + const data: StarknetTransactionData = { + senderAddress: contractAddress, + calls: [], + nonce, + chainId, + transactionType: StarknetTransactionType.DEPLOY_ACCOUNT, + resourceBounds: this._resourceBounds, + tip: this._tip, + transactionHash, + classHash: this._classHash, + constructorCalldata, + contractAddressSalt, + }; + + this._transaction.starknetTransactionData = data; + return this._transaction; + } + + private ensureDeployFields(): void { + if (!this._sender) { + throw new BuildTransactionError('Sender (counterfactual address) is required'); + } + if (this._publicKey && (!this._constructorCalldata || !this._contractAddressSalt)) { + this.fromPublicKey(this._publicKey); + } + if (!this._constructorCalldata || !this._contractAddressSalt) { + throw new BuildTransactionError( + 'Deploy account requires public key (fromPublicKey) or explicit constructor calldata and salt' + ); + } + const fullPublicKey = this._publicKey ? utils.getUncompressedPublicKey(this._publicKey) : undefined; + if (fullPublicKey) { + const derived = utils.computeStarknetAddress(fullPublicKey); + if (utils.normalizeAddress(derived.address) !== utils.normalizeAddress(this._sender)) { + throw new BuildTransactionError( + `Address does not match public key. Expected ${derived.address}, got ${this._sender}` + ); + } + if ( + derived.constructorCalldata.join(',') !== this._constructorCalldata.join(',') || + derived.salt !== this._contractAddressSalt + ) { + throw new BuildTransactionError('Constructor calldata or salt does not match public key'); + } + } + } +} diff --git a/modules/sdk-coin-starknet/test/unit/utils.ts b/modules/sdk-coin-starknet/test/unit/utils.ts index 3b1565764a..d6db407750 100644 --- a/modules/sdk-coin-starknet/test/unit/utils.ts +++ b/modules/sdk-coin-starknet/test/unit/utils.ts @@ -3,7 +3,10 @@ import utils, { getSelectorFromName, compileExecuteCalldata, calculateInvokeTransactionHash, + calculateDeployAccountTransactionHash, } from '../../src/lib/utils'; +import { coins } from '@bitgo/statics'; +import { TransactionBuilderFactory } from '../../src/lib/transactionBuilderFactory'; import { Accounts, SandboxTransferData, KnownGoodInvokeTx } from '../resources/starknet'; import { MASK_128 } from '../../src/lib/constants'; import 'should'; @@ -263,4 +266,44 @@ describe('Starknet Utils', () => { hash.should.equal(tv.expectedTxHash); }); }); + + describe('calculateDeployAccountTransactionHash', () => { + it('should be deterministic for the same deploy inputs', () => { + const fullPub = utils.getUncompressedPublicKey(Accounts.account1.publicKey); + const { address, constructorCalldata, salt } = utils.computeStarknetAddress(fullPub); + const params = { + contractAddress: address, + classHash: '0x3940bc18abf1df6bc540cabadb1cad9486c6803b95801e57b6153ae21abfe06', + constructorCalldata, + contractAddressSalt: salt, + chainId: SandboxTransferData.chainId, + nonce: '0x0', + resourceBounds: SandboxTransferData.resourceBounds, + tip: '0x0', + }; + const hash1 = calculateDeployAccountTransactionHash(params); + const hash2 = calculateDeployAccountTransactionHash(params); + hash1.should.equal(hash2); + hash1.should.startWith('0x'); + }); + + it('should match hash from WalletInitializationBuilder build', async () => { + const factory = new TransactionBuilderFactory(coins.get('starknet')); + const builder = factory.getWalletInitializationBuilder(); + builder.fromPublicKey(Accounts.account1.publicKey).nonce('0x0').chainId(SandboxTransferData.chainId); + const tx = (await builder.build()) as import('../../src/lib/transaction').Transaction; + const fullPub = utils.getUncompressedPublicKey(Accounts.account1.publicKey); + const { address, constructorCalldata, salt } = utils.computeStarknetAddress(fullPub); + const hash = calculateDeployAccountTransactionHash({ + contractAddress: address, + classHash: '0x3940bc18abf1df6bc540cabadb1cad9486c6803b95801e57b6153ae21abfe06', + constructorCalldata, + contractAddressSalt: salt, + chainId: SandboxTransferData.chainId, + nonce: '0x0', + resourceBounds: SandboxTransferData.resourceBounds, + }); + hash.should.equal(tx.starknetTransactionData.transactionHash); + }); + }); }); diff --git a/modules/sdk-coin-starknet/test/unit/walletInitializationBuilder.ts b/modules/sdk-coin-starknet/test/unit/walletInitializationBuilder.ts new file mode 100644 index 0000000000..0998a67c11 --- /dev/null +++ b/modules/sdk-coin-starknet/test/unit/walletInitializationBuilder.ts @@ -0,0 +1,146 @@ +import should from 'should'; +import { coins } from '@bitgo/statics'; +import { TransactionBuilderFactory } from '../../src/lib/transactionBuilderFactory'; +import { Transaction } from '../../src/lib/transaction'; +import { StarknetTransactionType } from '../../src/lib/iface'; +import { Accounts, SandboxTransferData } from '../resources/starknet'; +import { OZ_ETH_ACCOUNT_CLASS_HASH } from '../../src/lib/constants'; + +describe('Starknet WalletInitializationBuilder', () => { + const coinConfig = coins.get('starknet'); + const chainId = SandboxTransferData.chainId; + + describe('Build deploy account transaction', () => { + it('should build DEPLOY_ACCOUNT and produce a transactionHash', async () => { + const factory = new TransactionBuilderFactory(coinConfig); + const builder = factory.getWalletInitializationBuilder(); + + builder.fromPublicKey(Accounts.account1.publicKey).nonce('0x0').chainId(chainId); + + const tx = (await builder.build()) as Transaction; + const data = tx.starknetTransactionData; + + data.transactionType.should.equal(StarknetTransactionType.DEPLOY_ACCOUNT); + should.exist(data.transactionHash); + (data.transactionHash as string).should.startWith('0x'); + data.senderAddress.should.equal(Accounts.account1.address); + (data.classHash as string).should.equal(OZ_ETH_ACCOUNT_CLASS_HASH); + should.exist(data.constructorCalldata); + should.exist(data.contractAddressSalt); + data.calls.should.have.length(0); + }); + + it('should set signableHex from the Poseidon deploy hash', async () => { + const factory = new TransactionBuilderFactory(coinConfig); + const builder = factory.getWalletInitializationBuilder(); + + builder.fromPublicKey(Accounts.account1.publicKey).nonce('0x0').chainId(chainId); + + const tx = (await builder.build()) as Transaction; + tx.signableHex.should.equal(tx.starknetTransactionData.transactionHash); + tx.id.should.equal(tx.starknetTransactionData.transactionHash); + }); + + it('should produce different hashes for different accounts', async () => { + const factory = new TransactionBuilderFactory(coinConfig); + + const builder1 = factory.getWalletInitializationBuilder(); + builder1.fromPublicKey(Accounts.account1.publicKey).nonce('0x0').chainId(chainId); + const tx1 = (await builder1.build()) as Transaction; + + const builder2 = factory.getWalletInitializationBuilder(); + builder2.fromPublicKey(Accounts.account2.publicKey).nonce('0x0').chainId(chainId); + const tx2 = (await builder2.build()) as Transaction; + + tx1.signableHex.should.not.equal(tx2.signableHex); + }); + + it('should round-trip through toInternalHex and factory.from', async () => { + const factory = new TransactionBuilderFactory(coinConfig); + const builder = factory.getWalletInitializationBuilder(); + + builder.fromPublicKey(Accounts.account1.publicKey).nonce('0x0').chainId(chainId); + + const tx = (await builder.build()) as Transaction; + const internalHex = tx.toInternalHex(); + + const factory2 = new TransactionBuilderFactory(coinConfig); + const builder2 = await factory2.from(internalHex); + const tx2 = (await builder2.build()) as Transaction; + + tx2.signableHex.should.equal(tx.signableHex); + tx2.starknetTransactionData.transactionType.should.equal(StarknetTransactionType.DEPLOY_ACCOUNT); + }); + + it('toBroadcastFormat should return DEPLOY_ACCOUNT RPC JSON', async () => { + const factory = new TransactionBuilderFactory(coinConfig); + const builder = factory.getWalletInitializationBuilder(); + + builder.fromPublicKey(Accounts.account1.publicKey).nonce('0x0').chainId(chainId); + + const tx = (await builder.build()) as Transaction; + const broadcast = tx.toBroadcastFormat(); + const parsed = JSON.parse(broadcast); + + parsed.type.should.equal('DEPLOY_ACCOUNT'); + parsed.version.should.equal('0x3'); + parsed.sender_address.should.equal(Accounts.account1.address); + parsed.class_hash.should.equal(OZ_ETH_ACCOUNT_CLASS_HASH); + parsed.constructor_calldata.should.be.Array().and.not.empty(); + parsed.contract_address_salt.should.startWith('0x'); + parsed.nonce.should.equal('0x0'); + parsed.resource_bounds.should.have.property('l2_gas'); + parsed.nonce_data_availability_mode.should.equal('L1'); + parsed.fee_data_availability_mode.should.equal('L1'); + parsed.should.not.have.property('calldata'); + parsed.should.not.have.property('account_deployment_data'); + }); + }); + + describe('Validation', () => { + it('should reject build without public key or deploy fields', async () => { + const factory = new TransactionBuilderFactory(coinConfig); + const builder = factory.getWalletInitializationBuilder(); + builder.sender(Accounts.account1.address).nonce('0x0').chainId(chainId); + + await builder.build().should.be.rejectedWith(/public key|constructor calldata/i); + }); + + it('should reject mismatched address and public key', () => { + const factory = new TransactionBuilderFactory(coinConfig); + const builder = factory.getWalletInitializationBuilder(); + (() => + builder + .fromPublicKey(Accounts.account1.publicKey) + .sender(Accounts.account2.address, Accounts.account1.publicKey)).should.throw(/[Aa]ddress/); + }); + }); +}); + +describe('Starknet deploy account RPC wire format (live Sepolia)', () => { + it('should reach node validation (not param parse error) for unsigned deploy', async function (this: Mocha.Context) { + this.timeout(15000); + const factory = new TransactionBuilderFactory(coins.get('starknet')); + const builder = factory.getWalletInitializationBuilder(); + builder.fromPublicKey(Accounts.account1.publicKey).nonce('0x0').chainId(SandboxTransferData.chainId); + const tx = (await builder.build()) as Transaction; + const body = { + jsonrpc: '2.0', + method: 'starknet_addDeployAccountTransaction', + params: [JSON.parse(tx.toBroadcastFormat())], + id: 1, + }; + + const response = await fetch('https://api.cartridge.gg/x/starknet/sepolia', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }); + const json = (await response.json()) as { error?: { message?: string } }; + + should.exist(json.error); + const msg = json.error?.message || ''; + msg.should.not.match(/parsing params|EOF/i); + msg.should.match(/signature|Validate|nonce|fee|resource|Invalid params/i); + }); +}); From 6fc31f6707e39516b4ceb6b6bce72b6cabb6b2cf Mon Sep 17 00:00:00 2001 From: N V Rakesh Reddy Date: Wed, 10 Jun 2026 09:02:35 +0530 Subject: [PATCH 09/34] fix(sdk-core): populate recipients in buildTokenEnablements for TSS wallets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TSS wallets were missing `buildParams.recipients` after `buildTokenEnablements` because the TSS branch only called `prebuildTransaction(buildParams)` without first mapping `enableTokens → recipients`. The non-TSS branch correctly did this mapping. When `verifyTransaction` runs (`txParams = { ...txPrebuild.buildParams, ...params }`), `txParams.recipients` was undefined, causing `validateRawReceiver` in near.ts to throw "missing token name in transaction parameters". Unlike the non-TSS path, `enableTokens` is kept on `buildParams` for TSS since the server needs it to build the enableToken intent. Adds a regression test that exercises the real production call path — using `txPrebuild.buildParams` as `txParams` — which is what the existing TSS tests failed to do. TICKET: WP-5782 Co-Authored-By: Claude Sonnet 4.6 Co-authored-by: Cursor --- .../test/unit/tokenEnablementValidation.ts | 51 +++++++++++++++++++ modules/sdk-core/src/bitgo/wallet/wallet.ts | 14 +++++ 2 files changed, 65 insertions(+) diff --git a/modules/sdk-coin-near/test/unit/tokenEnablementValidation.ts b/modules/sdk-coin-near/test/unit/tokenEnablementValidation.ts index e6b68f3ac3..ba34e2333b 100644 --- a/modules/sdk-coin-near/test/unit/tokenEnablementValidation.ts +++ b/modules/sdk-coin-near/test/unit/tokenEnablementValidation.ts @@ -612,6 +612,57 @@ describe('NEAR Token Enablement Validation', function () { ); }); + it('should populate recipients from enableTokens in buildParams for TSS wallets', async function () { + // Regression test: before the fix, buildTokenEnablements on TSS wallets did not populate + // buildParams.recipients — only buildParams.enableTokens was set. This caused verifyTransaction + // to throw "missing token name in transaction parameters" because it reads from + // txParams.recipients[0].tokenName (where txParams = { ...txPrebuild.buildParams, ...params }). + const bgUrl = common.Environments['test'].uri; + + nock(bgUrl) + .post(`/api/v2/wallet/${tssWallet.id()}/txrequests`) + .reply(200, { + txRequestId: 'test-request-id', + apiVersion: 'full', + transactions: [ + { + state: 'pending', + unsignedTx: { + serializedTxHex: testData.rawTx.selfStorageDeposit.unsigned, + signableHex: testData.rawTx.selfStorageDeposit.unsigned, + derivationPath: 'm/0', + feeInfo: { fee: 1160407, feeString: '1160407' }, + }, + signatureShares: [], + }, + ], + }); + + const buildResult = await tssWallet.buildTokenEnablements({ + enableTokens: [{ name: 'tnear:tnep24dp' }], + }); + + const txPrebuild = buildResult[0] as any; + + // Verify buildParams.recipients is populated — this is what flows into txParams + // via { ...txPrebuild.buildParams, ...params } inside prebuildAndSignTransaction + txPrebuild.buildParams.should.have.property('recipients'); + txPrebuild.buildParams.recipients.should.have.length(1); + txPrebuild.buildParams.recipients[0].tokenName.should.equal('tnear:tnep24dp'); + txPrebuild.buildParams.recipients[0].address.should.equal(testData.accounts.account1.address); + + // Simulate the txParams construction that prebuildAndSignTransaction performs, + // then confirm verifyTransaction no longer throws "missing token name" + const txParams = { ...txPrebuild.buildParams }; + await basecoin.verifyTransaction({ + txParams, + txPrebuild, + wallet: tssWallet as any, + verification: { verifyTokenEnablement: true }, + walletType: 'tss', + }); + }); + it('should validate correct storage deposit in TSS wallet flow', async function () { const bgUrl = common.Environments['test'].uri; diff --git a/modules/sdk-core/src/bitgo/wallet/wallet.ts b/modules/sdk-core/src/bitgo/wallet/wallet.ts index 9fb0914c42..f19a6b86ad 100644 --- a/modules/sdk-core/src/bitgo/wallet/wallet.ts +++ b/modules/sdk-core/src/bitgo/wallet/wallet.ts @@ -3961,6 +3961,20 @@ export class Wallet implements IWallet { } // Check if we build with intent if (this._wallet.multisigType === 'tss') { + // Populate recipients from enableTokens so verifyTransaction can access tokenName. + // enableTokens is kept (not deleted) since the server needs it to build the transaction. + buildParams.recipients = params.enableTokens.map((token) => { + const address = + token.address || this._wallet.coinSpecific?.baseAddress || this._wallet.coinSpecific?.rootAddress; + if (!address) { + throw new Error('Wallet does not have base address, must specify with token param'); + } + return { + tokenName: token.name, + address, + amount: '0', + }; + }); return [await this.prebuildTransaction(buildParams)]; } else { // Rewrite tokens into recipients for buildTransaction From 758c712d67674b0d16f3fee5bcb4db879c9eecb5 Mon Sep 17 00:00:00 2001 From: Harsh Chawla Date: Wed, 10 Jun 2026 09:33:42 +0530 Subject: [PATCH 10/34] feat: onboards spaceX token TICKET: SCAAS-9660 --- modules/statics/src/base.ts | 5 +++-- modules/statics/src/coins/ofcCoins.ts | 26 +++++++++++++++++++------- modules/statics/src/coins/solTokens.ts | 23 +++++++++++++++++------ 3 files changed, 39 insertions(+), 15 deletions(-) diff --git a/modules/statics/src/base.ts b/modules/statics/src/base.ts index 6574cf4261..612eb08669 100644 --- a/modules/statics/src/base.ts +++ b/modules/statics/src/base.ts @@ -1214,8 +1214,9 @@ export enum UnderlyingAsset { 'tsol:sofid' = 'tsol:sofid', 'tsol:stgsofid' = 'tsol:stgsofid', 'sol:sofid' = 'sol:sofid', - 'tsol:spcx' = 'tsol:spcx', - 'tsol:stgspcx' = 'tsol:stgspcx', + 'tsol:gospcx' = 'tsol:gospcx', + 'tsol:stggospcx' = 'tsol:stggospcx', + 'sol:gospcx' = 'sol:gospcx', 'sol:usd1' = 'sol:usd1', 'sol:usdm1' = 'sol:usdm1', 'tsol:slnd' = 'tsol:slnd', diff --git a/modules/statics/src/coins/ofcCoins.ts b/modules/statics/src/coins/ofcCoins.ts index 99a8084d28..0493d2edac 100644 --- a/modules/statics/src/coins/ofcCoins.ts +++ b/modules/statics/src/coins/ofcCoins.ts @@ -1618,6 +1618,14 @@ export const ofcCoins = [ ...SOL_TOKEN_FEATURES, CoinFeature.STABLECOIN, ]), + ofcsolToken( + 'b53025fd-20e7-4a61-8893-2f26ed2daa7b', + 'ofcsol:gospcx', + 'SPCX goStock', + 6, + UnderlyingAsset['sol:gospcx'], + [...SOL_TOKEN_FEATURES, CoinFeature.STABLECOIN] + ), ofcsolToken( 'e343b3c2-dcbb-4a9f-a60e-3dd79825c5fb', 'ofcsol:rksol', @@ -1994,16 +2002,20 @@ export const ofcCoins = [ ), tofcsolToken( '3441411c-d379-4f61-9630-1e8de117717a', - 'ofctsol:stgspcx', - 'Test SpaceX', + 'ofctsol:stggospcx', + 'Test SPCX goStock', 6, - UnderlyingAsset['tsol:stgspcx'], + UnderlyingAsset['tsol:stggospcx'], + [...SOL_TOKEN_FEATURES, CoinFeature.STABLECOIN] + ), + tofcsolToken( + '84f901ed-5654-47a7-9d08-7ff7c27a8c16', + 'ofctsol:gospcx', + 'Test SPCX goStock', + 6, + UnderlyingAsset['tsol:gospcx'], [...SOL_TOKEN_FEATURES, CoinFeature.STABLECOIN] ), - tofcsolToken('84f901ed-5654-47a7-9d08-7ff7c27a8c16', 'ofctsol:spcx', 'Test SpaceX', 6, UnderlyingAsset['tsol:spcx'], [ - ...SOL_TOKEN_FEATURES, - CoinFeature.STABLECOIN, - ]), tofcsolToken( '750f0e40-c5b9-464f-874f-dc455cf1494b', 'ofctsol:stgusd1', diff --git a/modules/statics/src/coins/solTokens.ts b/modules/statics/src/coins/solTokens.ts index e774c05331..e4110dc170 100644 --- a/modules/statics/src/coins/solTokens.ts +++ b/modules/statics/src/coins/solTokens.ts @@ -3650,23 +3650,34 @@ export const solTokens = [ ), tsolToken( '70f8706b-d4c8-49ac-a8ae-ea1c3c60249e', - 'tsol:stgspcx', - 'Test SpaceX', + 'tsol:stggospcx', + 'Test SPCX goStock', 6, '98iaHRfvCnaihEHcLK5EpwLq3w5yHiKKfSgpYLcxEScB', '98iaHRfvCnaihEHcLK5EpwLq3w5yHiKKfSgpYLcxEScB', - UnderlyingAsset['tsol:stgspcx'], + UnderlyingAsset['tsol:stggospcx'], [...SOL_TOKEN_FEATURES, CoinFeature.STABLECOIN], ProgramID.Token2022ProgramId ), tsolToken( '44e9960a-e7ed-4ae5-8cc9-f16de5744866', - 'tsol:spcx', - 'Test SpaceX', + 'tsol:gospcx', + 'Test SPCX goStock', 6, '4gvEw3Lx2gkAByv4X8hLnHujneN5bwtMGSu2ZDoip4vj', '4gvEw3Lx2gkAByv4X8hLnHujneN5bwtMGSu2ZDoip4vj', - UnderlyingAsset['tsol:spcx'], + UnderlyingAsset['tsol:gospcx'], + [...SOL_TOKEN_FEATURES, CoinFeature.STABLECOIN], + ProgramID.Token2022ProgramId + ), + solToken( + '83ec93bc-e142-4c32-874a-65f5b8974548', + 'sol:gospcx', + 'SPCX goStock', + 6, + 'AAVvaNDwkGfxGNaf1HJ5JzfwDb1PYmAgXSixRsczyrk4', + 'AAVvaNDwkGfxGNaf1HJ5JzfwDb1PYmAgXSixRsczyrk4', + UnderlyingAsset['sol:gospcx'], [...SOL_TOKEN_FEATURES, CoinFeature.STABLECOIN], ProgramID.Token2022ProgramId ), From 54c9627c860b70193d898feaa9c42d48558ca6fe Mon Sep 17 00:00:00 2001 From: prithvishet2503 Date: Wed, 10 Jun 2026 09:43:36 +0530 Subject: [PATCH 11/34] feat: introduce SUNSETTING coin feature for SEI Ticket: CECHO-1297 TICKET: CECHO-1297 --- modules/statics/src/base.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/modules/statics/src/base.ts b/modules/statics/src/base.ts index 6574cf4261..3a99cdc1df 100644 --- a/modules/statics/src/base.ts +++ b/modules/statics/src/base.ts @@ -272,6 +272,10 @@ export enum CoinFeature { * This coin is deprecated */ DEPRECATED = 'deprecated', + /** + * This coin is sunsetting and will be deprecated in the near future, but is not deprecated yet + */ + SUNSETTING = 'sunsetting', /** * This coin is a dummy object meant to be a placeholder for an unsupported token */ From 521168ccad5ef016f89e3467c4f67daa58b1a13c Mon Sep 17 00:00:00 2001 From: "vignesh285@bitgo.com" Date: Wed, 10 Jun 2026 06:21:24 +0000 Subject: [PATCH 12/34] ci: fix incorrect pinned SHA for EricCrosson/retry@v1.4.8 Update checksum from 15224553f40d5d16dcc1a696798741227c79670a41f43e522002e634aa1d7c64 to d207746ff0eda67c706df25e88c02520f0cf3172279eb8eec8224fb0d3558911 in both publish.yml and npmjs-release.yml. Ticket: VL-6482 Session-Id: 97fea898-2e16-47ff-b0b5-47798132a382 Task-Id: 3e3641a7-3d6a-483a-9b74-84f4ed665323 --- .github/workflows/npmjs-release.yml | 2 +- .github/workflows/publish.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/npmjs-release.yml b/.github/workflows/npmjs-release.yml index ab3ef1e1c6..03f9477087 100644 --- a/.github/workflows/npmjs-release.yml +++ b/.github/workflows/npmjs-release.yml @@ -239,7 +239,7 @@ jobs: - name: Install retry uses: BitGo/install-github-release-binary@v2 with: - targets: EricCrosson/retry@v1.4.8:sha256-15224553f40d5d16dcc1a696798741227c79670a41f43e522002e634aa1d7c64 + targets: EricCrosson/retry@v1.4.8:sha256-d207746ff0eda67c706df25e88c02520f0cf3172279eb8eec8224fb0d3558911 - name: Run yarn audit run: retry --up-to 2x --every 3s -- yarn run audit-high --retry-on-network-failure diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index f7239ea240..132104e1bf 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -39,7 +39,7 @@ jobs: - name: Install retry uses: BitGo/install-github-release-binary@v2 with: - targets: EricCrosson/retry@v1.4.8:sha256-15224553f40d5d16dcc1a696798741227c79670a41f43e522002e634aa1d7c64 + targets: EricCrosson/retry@v1.4.8:sha256-d207746ff0eda67c706df25e88c02520f0cf3172279eb8eec8224fb0d3558911 - name: Audit Dependencies run: retry --up-to 2x --every 3s -- yarn run improved-yarn-audit --min-severity high --retry-on-network-failure From 52df0da41a49c703fed70a6991566b40c1ebd25a Mon Sep 17 00:00:00 2001 From: Ravi Hegde Date: Wed, 10 Jun 2026 12:30:00 +0530 Subject: [PATCH 13/34] feat(sdk-core): added canton types in tss prebuild flow Ticket: CHALO-573 --- modules/sdk-coin-canton/src/lib/utils.ts | 12 +++--- modules/sdk-coin-canton/test/resources.ts | 11 +++++ .../cosignDelegationAcceptBuilder.ts | 42 ++++++++++++++++++- modules/sdk-core/src/bitgo/utils/mpcUtils.ts | 15 ++++++- modules/sdk-core/src/bitgo/wallet/wallet.ts | 39 +++++++++++++++++ 5 files changed, 109 insertions(+), 10 deletions(-) diff --git a/modules/sdk-coin-canton/src/lib/utils.ts b/modules/sdk-coin-canton/src/lib/utils.ts index c5d94cf8bb..13cecca6a3 100644 --- a/modules/sdk-coin-canton/src/lib/utils.ts +++ b/modules/sdk-coin-canton/src/lib/utils.ts @@ -352,13 +352,13 @@ export class Utils implements BaseUtils { } case TransactionType.CosignDelegationAccept: { - // exercise CosignDelegationProposal_Accept → actingParties[0] = signer (sender) - const signerParty = findExerciseActingParty('CosignDelegationProposal_Accept'); + // exercise CosigningDelegationProposal_Accept → actingParties[0] = signer (sender) + const signerParty = findExerciseActingParty('CosigningDelegationProposal_Accept'); if (signerParty) sender = signerParty; - // CosignDelegationProposal create node → admin = receiver - const proposalFields = findCreateNodeFields('CosignDelegationProposal'); - if (proposalFields) { - const adminData = getField(proposalFields, 'admin'); + // CosigningDelegation create node (result of the accept) → admin = receiver + const delegationFields = findCreateNodeFields('CosigningDelegation'); + if (delegationFields) { + const adminData = getField(delegationFields, 'admin'); if (adminData?.oneofKind === 'party') receiver = adminData.party ?? ''; } amount = '0'; diff --git a/modules/sdk-coin-canton/test/resources.ts b/modules/sdk-coin-canton/test/resources.ts index dc8718040f..d1774789a0 100644 --- a/modules/sdk-coin-canton/test/resources.ts +++ b/modules/sdk-coin-canton/test/resources.ts @@ -348,3 +348,14 @@ export const CantonCreateCommandPrepareResponse = { preparedTransactionHash: 'xn2fK57XUY7MFHWAsppKczOkgUYx//0VyMC1jBNbuPI=', hashingSchemeVersion: 'HASHING_SCHEME_VERSION_V2', }; + +export const CosignDelegationAcceptPrepareResponse = { + preparedTransaction: + 'CqcPCgMyLjESATAaygYKATHCPsMGCsAGCgMyLjESQjAwMWJkYTEwYzA4ZTFmNDVlZTVlZTFlMmFjYmUzOTkwNGJhMWJmOTczNmM4MTRiYTgxNDNjMWQyNGY2ZTYyODkwNxoPdHJhZGV3ZWItZGFkLXYxIm4KQDgyYzEwNzVhMzA3NTFlOTEzYTBlNzcxM2M0MDVlNmZjOWVjZGIzNmEwMTI1NDVlMWY4YjMxMDA0MDdiYjQ4ZTMSFVRXLk9wZXJhdG9yLlYxLkNvc2lnbhoTQ29zaWduaW5nRGVsZWdhdGlvbiqtAnKqAgpuCkA4MmMxMDc1YTMwNzUxZTkxM2EwZTc3MTNjNDA1ZTZmYzllY2RiMzZhMDEyNTQ1ZTFmOGIzMTAwNDA3YmI0OGUzEhVUVy5PcGVyYXRvci5WMS5Db3NpZ24aE0Nvc2lnbmluZ0RlbGVnYXRpb24SXwoFYWRtaW4SVjpUcmF2aS1uZXctcGFydHk6OjEyMjA5MmU3ZDMzYWMxMGMwZjNkNTU5NzYzNDJmMzc1NTVkZjA1ZGE1Yjc0Mjk1NmQ1NmE2MmFlMjM2Nzc2OTA3OWQyElcKBnNpZ25lchJNOksxMjIwODo6MTIyMDgzMDgyZTlhZjE1NmZlYWViN2FmZDM2M2EwZWU1ZmZhMWZkMTYwOTQ3YjY0N2ExMzlhN2UwYzJlZDc4ZjVkYzcySzEyMjA4OjoxMjIwODMwODJlOWFmMTU2ZmVhZWI3YWZkMzYzYTBlZTVmZmExZmQxNjA5NDdiNjQ3YTEzOWE3ZTBjMmVkNzhmNWRjNzJUcmF2aS1uZXctcGFydHk6OjEyMjA5MmU3ZDMzYWMxMGMwZjNkNTU5NzYzNDJmMzc1NTVkZjA1ZGE1Yjc0Mjk1NmQ1NmE2MmFlMjM2Nzc2OTA3OWQyOksxMjIwODo6MTIyMDgzMDgyZTlhZjE1NmZlYWViN2FmZDM2M2EwZWU1ZmZhMWZkMTYwOTQ3YjY0N2ExMzlhN2UwYzJlZDc4ZjVkYzc6VHJhdmktbmV3LXBhcnR5OjoxMjIwOTJlN2QzM2FjMTBjMGYzZDU1OTc2MzQyZjM3NTU1ZGYwNWRhNWI3NDI5NTZkNTZhNjJhZTIzNjc3NjkwNzlkMhqFCAoBMMI+/gca+wcKAzIuMRKKATAwNTBjNjIzMmQzOWU1ZDFlNGQ5MTZiMmRiZTlkM2Q2NGFkZTc0YjQyZDliOTQyODkxNjhiMTM4NmE1NjNkYTU5MWNhMTIxMjIwNTMyMDdhYWFkYmZiYThlYTQ5NGY5NWVhY2IxMTA4YzVhZjc2ZWZiNDk5ODk3MWYyYzljZDc2NTk1NjU3NzA4NBoPdHJhZGV3ZWItZGFkLXYxInYKQDgyYzEwNzVhMzA3NTFlOTEzYTBlNzcxM2M0MDVlNmZjOWVjZGIzNmEwMTI1NDVlMWY4YjMxMDA0MDdiYjQ4ZTMSFVRXLk9wZXJhdG9yLlYxLkNvc2lnbhobQ29zaWduaW5nRGVsZWdhdGlvblByb3Bvc2FsKlRyYXZpLW5ldy1wYXJ0eTo6MTIyMDkyZTdkMzNhYzEwYzBmM2Q1NTk3NjM0MmYzNzU1NWRmMDVkYTViNzQyOTU2ZDU2YTYyYWUyMzY3NzY5MDc5ZDIySzEyMjA4OjoxMjIwODMwODJlOWFmMTU2ZmVhZWI3YWZkMzYzYTBlZTVmZmExZmQxNjA5NDdiNjQ3YTEzOWE3ZTBjMmVkNzhmNWRjNzJUcmF2aS1uZXctcGFydHk6OjEyMjA5MmU3ZDMzYWMxMGMwZjNkNTU5NzYzNDJmMzc1NTVkZjA1ZGE1Yjc0Mjk1NmQ1NmE2MmFlMjM2Nzc2OTA3OWQyOksxMjIwODo6MTIyMDgzMDgyZTlhZjE1NmZlYWViN2FmZDM2M2EwZWU1ZmZhMWZkMTYwOTQ3YjY0N2ExMzlhN2UwYzJlZDc4ZjVkYzdKIkNvc2lnbmluZ0RlbGVnYXRpb25Qcm9wb3NhbF9BY2NlcHRSgQFyfwp9CkA4MmMxMDc1YTMwNzUxZTkxM2EwZTc3MTNjNDA1ZTZmYzllY2RiMzZhMDEyNTQ1ZTFmOGIzMTAwNDA3YmI0OGUzEhVUVy5PcGVyYXRvci5WMS5Db3NpZ24aIkNvc2lnbmluZ0RlbGVnYXRpb25Qcm9wb3NhbF9BY2NlcHRYAWIBMWrqAXLnAQqEAQpAODJjMTA3NWEzMDc1MWU5MTNhMGU3NzEzYzQwNWU2ZmM5ZWNkYjM2YTAxMjU0NWUxZjhiMzEwMDQwN2JiNDhlMxIVVFcuT3BlcmF0b3IuVjEuQ29zaWduGilDb3NpZ25pbmdEZWxlZ2F0aW9uUHJvcG9zYWxfQWNjZXB0X1Jlc3VsdBJeChZjb3NpZ25pbmdEZWxlZ2F0aW9uQ2lkEkRKQjAwMWJkYTEwYzA4ZTFmNDVlZTVlZTFlMmFjYmUzOTkwNGJhMWJmOTczNmM4MTRiYTgxNDNjMWQyNGY2ZTYyODkwNyIiEiDexREPEP5Xv4zbEY2fCbOcVZi0Df7jF2cc4nnu2wJtsSIkCAESIJs7xUanIWWT1wZipHMBsulvXBnAE6R1amIRgSwCiBjUEr0NEnMKSzEyMjA4OjoxMjIwODMwODJlOWFmMTU2ZmVhZWI3YWZkMzYzYTBlZTVmZmExZmQxNjA5NDdiNjQ3YTEzOWE3ZTBjMmVkNzhmNWRjNxIkYTQyOTIyNTQtMzYxMC00ZGMyLTkzZTAtNTkwOGZiOWNjZTFmGlNnbG9iYWwtZG9tYWluOjoxMjIwYmU1OGMyOWU2NWRlNDBiZjI3M2JlMWRjMmIyNjZkNDNhOWEwMDJlYTViMTg5NTVhZWVmN2FhYzg4MWJiNDcxYSokNTBiNWRkODAtYWFhOS00YzJlLTkzNjMtNTUzOTA1NjFlMWU1MKjThsOO/JQDOsELCswGCgMyLjESigEwMDUwYzYyMzJkMzllNWQxZTRkOTE2YjJkYmU5ZDNkNjRhZGU3NGI0MmQ5Yjk0Mjg5MTY4YjEzODZhNTYzZGE1OTFjYTEyMTIyMDUzMjA3YWFhZGJmYmE4ZWE0OTRmOTVlYWNiMTEwOGM1YWY3NmVmYjQ5OTg5NzFmMmM5Y2Q3NjU5NTY1NzcwODQaD3RyYWRld2ViLWRhZC12MSJ2CkA4MmMxMDc1YTMwNzUxZTkxM2EwZTc3MTNjNDA1ZTZmYzllY2RiMzZhMDEyNTQ1ZTFmOGIzMTAwNDA3YmI0OGUzEhVUVy5PcGVyYXRvci5WMS5Db3NpZ24aG0Nvc2lnbmluZ0RlbGVnYXRpb25Qcm9wb3NhbCq1AnKyAgp2CkA4MmMxMDc1YTMwNzUxZTkxM2EwZTc3MTNjNDA1ZTZmYzllY2RiMzZhMDEyNTQ1ZTFmOGIzMTAwNDA3YmI0OGUzEhVUVy5PcGVyYXRvci5WMS5Db3NpZ24aG0Nvc2lnbmluZ0RlbGVnYXRpb25Qcm9wb3NhbBJfCgVhZG1pbhJWOlRyYXZpLW5ldy1wYXJ0eTo6MTIyMDkyZTdkMzNhYzEwYzBmM2Q1NTk3NjM0MmYzNzU1NWRmMDVkYTViNzQyOTU2ZDU2YTYyYWUyMzY3NzY5MDc5ZDISVwoGc2lnbmVyEk06SzEyMjA4OjoxMjIwODMwODJlOWFmMTU2ZmVhZWI3YWZkMzYzYTBlZTVmZmExZmQxNjA5NDdiNjQ3YTEzOWE3ZTBjMmVkNzhmNWRjNzJUcmF2aS1uZXctcGFydHk6OjEyMjA5MmU3ZDMzYWMxMGMwZjNkNTU5NzYzNDJmMzc1NTVkZjA1ZGE1Yjc0Mjk1NmQ1NmE2MmFlMjM2Nzc2OTA3OWQyOksxMjIwODo6MTIyMDgzMDgyZTlhZjE1NmZlYWViN2FmZDM2M2EwZWU1ZmZhMWZkMTYwOTQ3YjY0N2ExMzlhN2UwYzJlZDc4ZjVkYzc6VHJhdmktbmV3LXBhcnR5OjoxMjIwOTJlN2QzM2FjMTBjMGYzZDU1OTc2MzQyZjM3NTU1ZGYwNWRhNWI3NDI5NTZkNTZhNjJhZTIzNjc3NjkwNzlkMsA+ttP6pP/7lAPSPuQECgMyLjES3AQKRQBQxiMtOeXR5NkWstvp09ZK3nS0LZuUKJFosThqVj2lkcoSEiBTIHqq2/uo6klPlerLEQjFr3bvtJmJcfLJzXZZVldwhBIPdHJhZGV3ZWItZGFkLXYxGnkKQDgyYzEwNzVhMzA3NTFlOTEzYTBlNzcxM2M0MDVlNmZjOWVjZGIzNmEwMTI1NDVlMWY4YjMxMDA0MDdiYjQ4ZTMSAlRXEghPcGVyYXRvchICVjESBkNvc2lnbhobQ29zaWduaW5nRGVsZWdhdGlvblByb3Bvc2FsIq4BaqsBClgKVjpUcmF2aS1uZXctcGFydHk6OjEyMjA5MmU3ZDMzYWMxMGMwZjNkNTU5NzYzNDJmMzc1NTVkZjA1ZGE1Yjc0Mjk1NmQ1NmE2MmFlMjM2Nzc2OTA3OWQyCk8KTTpLMTIyMDg6OjEyMjA4MzA4MmU5YWYxNTZmZWFlYjdhZmQzNjNhMGVlNWZmYTFmZDE2MDk0N2I2NDdhMTM5YTdlMGMyZWQ3OGY1ZGM3KlRyYXZpLW5ldy1wYXJ0eTo6MTIyMDkyZTdkMzNhYzEwYzBmM2Q1NTk3NjM0MmYzNzU1NWRmMDVkYTViNzQyOTU2ZDU2YTYyYWUyMzY3NzY5MDc5ZDIySzEyMjA4OjoxMjIwODMwODJlOWFmMTU2ZmVhZWI3YWZkMzYzYTBlZTVmZmExZmQxNjA5NDdiNjQ3YTEzOWE3ZTBjMmVkNzhmNWRjNzm2qZ7031MGAEIqCiYKJAgBEiBDS185jpVaGFmJ2vX54nF3ZtTWTXiSUFXL/dL5ESXNlhAe', + preparedTransactionHash: 'd6iTd+VjYA/cJw7pj+CaKVSs0MqMXV9LZNU45Ij1188=', + hashingSchemeVersion: 'HASHING_SCHEME_VERSION_V2', + hashingDetails: null, +}; + +export const CosignDelegationAcceptRawTransaction = + 'eyJwcmVwYXJlQ29tbWFuZFJlc3BvbnNlIjp7InByZXBhcmVkVHJhbnNhY3Rpb24iOiJDcWNQQ2dNeUxqRVNBVEFheWdZS0FUSENQc01HQ3NBR0NnTXlMakVTUWpBd01XSmtZVEV3WXpBNFpURm1ORFZsWlRWbFpURmxNbUZqWW1Vek9Ua3dOR0poTVdKbU9UY3pObU00TVRSaVlUZ3hORE5qTVdReU5HWTJaVFl5T0Rrd054b1BkSEpoWkdWM1pXSXRaR0ZrTFhZeEltNEtRRGd5WXpFd056VmhNekEzTlRGbE9URXpZVEJsTnpjeE0yTTBNRFZsTm1aak9XVmpaR0l6Tm1Fd01USTFORFZsTVdZNFlqTXhNREEwTURkaVlqUTRaVE1TRlZSWExrOXdaWEpoZEc5eUxsWXhMa052YzJsbmJob1RRMjl6YVdkdWFXNW5SR1ZzWldkaGRHbHZiaXF0QW5LcUFncHVDa0E0TW1NeE1EYzFZVE13TnpVeFpUa3hNMkV3WlRjM01UTmpOREExWlRabVl6bGxZMlJpTXpaaE1ERXlOVFExWlRGbU9HSXpNVEF3TkRBM1ltSTBPR1V6RWhWVVZ5NVBjR1Z5WVhSdmNpNVdNUzVEYjNOcFoyNGFFME52YzJsbmJtbHVaMFJsYkdWbllYUnBiMjRTWHdvRllXUnRhVzRTVmpwVWNtRjJhUzF1WlhjdGNHRnlkSGs2T2pFeU1qQTVNbVUzWkRNellXTXhNR013WmpOa05UVTVOell6TkRKbU16YzFOVFZrWmpBMVpHRTFZamMwTWprMU5tUTFObUUyTW1GbE1qTTJOemMyT1RBM09XUXlFbGNLQm5OcFoyNWxjaEpOT2tzeE1qSXdPRG82TVRJeU1EZ3pNRGd5WlRsaFpqRTFObVpsWVdWaU4yRm1aRE0yTTJFd1pXVTFabVpoTVdaa01UWXdPVFEzWWpZME4yRXhNemxoTjJVd1l6SmxaRGM0WmpWa1l6Y3lTekV5TWpBNE9qb3hNakl3T0RNd09ESmxPV0ZtTVRVMlptVmhaV0kzWVdaa016WXpZVEJsWlRWbVptRXhabVF4TmpBNU5EZGlOalEzWVRFek9XRTNaVEJqTW1Wa056aG1OV1JqTnpKVWNtRjJhUzF1WlhjdGNHRnlkSGs2T2pFeU1qQTVNbVUzWkRNellXTXhNR013WmpOa05UVTVOell6TkRKbU16YzFOVFZrWmpBMVpHRTFZamMwTWprMU5tUTFObUUyTW1GbE1qTTJOemMyT1RBM09XUXlPa3N4TWpJd09EbzZNVEl5TURnek1EZ3laVGxoWmpFMU5tWmxZV1ZpTjJGbVpETTJNMkV3WldVMVptWmhNV1prTVRZd09UUTNZalkwTjJFeE16bGhOMlV3WXpKbFpEYzRaalZrWXpjNlZISmhkbWt0Ym1WM0xYQmhjblI1T2pveE1qSXdPVEpsTjJRek0yRmpNVEJqTUdZelpEVTFPVGMyTXpReVpqTTNOVFUxWkdZd05XUmhOV0kzTkRJNU5UWmtOVFpoTmpKaFpUSXpOamMzTmprd056bGtNaHFGQ0FvQk1NSSsvZ2NhK3djS0F6SXVNUktLQVRBd05UQmpOakl6TW1Rek9XVTFaREZsTkdRNU1UWmlNbVJpWlRsa00yUTJOR0ZrWlRjMFlqUXlaRGxpT1RReU9Ea3hOamhpTVRNNE5tRTFOak5rWVRVNU1XTmhNVEl4TWpJd05UTXlNRGRoWVdGa1ltWmlZVGhsWVRRNU5HWTVOV1ZoWTJJeE1UQTRZelZoWmpjMlpXWmlORGs1T0RrM01XWXlZemxqWkRjMk5UazFOalUzTnpBNE5Cb1BkSEpoWkdWM1pXSXRaR0ZrTFhZeEluWUtRRGd5WXpFd056VmhNekEzTlRGbE9URXpZVEJsTnpjeE0yTTBNRFZsTm1aak9XVmpaR0l6Tm1Fd01USTFORFZsTVdZNFlqTXhNREEwTURkaVlqUTRaVE1TRlZSWExrOXdaWEpoZEc5eUxsWXhMa052YzJsbmJob2JRMjl6YVdkdWFXNW5SR1ZzWldkaGRHbHZibEJ5YjNCdmMyRnNLbFJ5WVhacExXNWxkeTF3WVhKMGVUbzZNVEl5TURreVpUZGtNek5oWXpFd1l6Qm1NMlExTlRrM05qTTBNbVl6TnpVMU5XUm1NRFZrWVRWaU56UXlPVFUyWkRVMllUWXlZV1V5TXpZM056WTVNRGM1WkRJeVN6RXlNakE0T2pveE1qSXdPRE13T0RKbE9XRm1NVFUyWm1WaFpXSTNZV1prTXpZellUQmxaVFZtWm1FeFptUXhOakE1TkRkaU5qUTNZVEV6T1dFM1pUQmpNbVZrTnpobU5XUmpOekpVY21GMmFTMXVaWGN0Y0dGeWRIazZPakV5TWpBNU1tVTNaRE16WVdNeE1HTXdaak5rTlRVNU56WXpOREptTXpjMU5UVmtaakExWkdFMVlqYzBNamsxTm1RMU5tRTJNbUZsTWpNMk56YzJPVEEzT1dReU9rc3hNakl3T0RvNk1USXlNRGd6TURneVpUbGhaakUxTm1abFlXVmlOMkZtWkRNMk0yRXdaV1UxWm1aaE1XWmtNVFl3T1RRM1lqWTBOMkV4TXpsaE4yVXdZekpsWkRjNFpqVmtZemRLSWtOdmMybG5ibWx1WjBSbGJHVm5ZWFJwYjI1UWNtOXdiM05oYkY5QlkyTmxjSFJTZ1FGeWZ3cDlDa0E0TW1NeE1EYzFZVE13TnpVeFpUa3hNMkV3WlRjM01UTmpOREExWlRabVl6bGxZMlJpTXpaaE1ERXlOVFExWlRGbU9HSXpNVEF3TkRBM1ltSTBPR1V6RWhWVVZ5NVBjR1Z5WVhSdmNpNVdNUzVEYjNOcFoyNGFJa052YzJsbmJtbHVaMFJsYkdWbllYUnBiMjVRY205d2IzTmhiRjlCWTJObGNIUllBV0lCTVdycUFYTG5BUXFFQVFwQU9ESmpNVEEzTldFek1EYzFNV1U1TVROaE1HVTNOekV6WXpRd05XVTJabU01WldOa1lqTTJZVEF4TWpVME5XVXhaamhpTXpFd01EUXdOMkppTkRobE14SVZWRmN1VDNCbGNtRjBiM0l1VmpFdVEyOXphV2R1R2lsRGIzTnBaMjVwYm1kRVpXeGxaMkYwYVc5dVVISnZjRzl6WVd4ZlFXTmpaWEIwWDFKbGMzVnNkQkplQ2haamIzTnBaMjVwYm1kRVpXeGxaMkYwYVc5dVEybGtFa1JLUWpBd01XSmtZVEV3WXpBNFpURm1ORFZsWlRWbFpURmxNbUZqWW1Vek9Ua3dOR0poTVdKbU9UY3pObU00TVRSaVlUZ3hORE5qTVdReU5HWTJaVFl5T0Rrd055SWlFaURleFJFUEVQNVh2NHpiRVkyZkNiT2NWWmkwRGY3akYyY2M0bm51MndKdHNTSWtDQUVTSUpzN3hVYW5JV1dUMXdaaXBITUJzdWx2WEJuQUU2UjFhbUlSZ1N3Q2lCalVFcjBORW5NS1N6RXlNakE0T2pveE1qSXdPRE13T0RKbE9XRm1NVFUyWm1WaFpXSTNZV1prTXpZellUQmxaVFZtWm1FeFptUXhOakE1TkRkaU5qUTNZVEV6T1dFM1pUQmpNbVZrTnpobU5XUmpOeElrWVRReU9USXlOVFF0TXpZeE1DMDBaR015TFRrelpUQXROVGt3T0daaU9XTmpaVEZtR2xObmJHOWlZV3d0Wkc5dFlXbHVPam94TWpJd1ltVTFPR015T1dVMk5XUmxOREJpWmpJM00ySmxNV1JqTW1JeU5qWmtORE5oT1dFd01ESmxZVFZpTVRnNU5UVmhaV1ZtTjJGaFl6ZzRNV0ppTkRjeFlTb2tOVEJpTldSa09EQXRZV0ZoT1MwMFl6SmxMVGt6TmpNdE5UVXpPVEExTmpGbE1XVTFNS2pUaHNPTy9KUURPc0VMQ3N3R0NnTXlMakVTaWdFd01EVXdZell5TXpKa016bGxOV1F4WlRSa09URTJZakprWW1VNVpETmtOalJoWkdVM05HSTBNbVE1WWprME1qZzVNVFk0WWpFek9EWmhOVFl6WkdFMU9URmpZVEV5TVRJeU1EVXpNakEzWVdGaFpHSm1ZbUU0WldFME9UUm1PVFZsWVdOaU1URXdPR00xWVdZM05tVm1ZalE1T1RnNU56Rm1NbU01WTJRM05qVTVOVFkxTnpjd09EUWFEM1J5WVdSbGQyVmlMV1JoWkMxMk1TSjJDa0E0TW1NeE1EYzFZVE13TnpVeFpUa3hNMkV3WlRjM01UTmpOREExWlRabVl6bGxZMlJpTXpaaE1ERXlOVFExWlRGbU9HSXpNVEF3TkRBM1ltSTBPR1V6RWhWVVZ5NVBjR1Z5WVhSdmNpNVdNUzVEYjNOcFoyNGFHME52YzJsbmJtbHVaMFJsYkdWbllYUnBiMjVRY205d2IzTmhiQ3ExQW5LeUFncDJDa0E0TW1NeE1EYzFZVE13TnpVeFpUa3hNMkV3WlRjM01UTmpOREExWlRabVl6bGxZMlJpTXpaaE1ERXlOVFExWlRGbU9HSXpNVEF3TkRBM1ltSTBPR1V6RWhWVVZ5NVBjR1Z5WVhSdmNpNVdNUzVEYjNOcFoyNGFHME52YzJsbmJtbHVaMFJsYkdWbllYUnBiMjVRY205d2IzTmhiQkpmQ2dWaFpHMXBiaEpXT2xSeVlYWnBMVzVsZHkxd1lYSjBlVG82TVRJeU1Ea3laVGRrTXpOaFl6RXdZekJtTTJRMU5UazNOak0wTW1Zek56VTFOV1JtTURWa1lUVmlOelF5T1RVMlpEVTJZVFl5WVdVeU16WTNOelk1TURjNVpESVNWd29HYzJsbmJtVnlFazA2U3pFeU1qQTRPam94TWpJd09ETXdPREpsT1dGbU1UVTJabVZoWldJM1lXWmtNell6WVRCbFpUVm1abUV4Wm1ReE5qQTVORGRpTmpRM1lURXpPV0UzWlRCak1tVmtOemhtTldSak56SlVjbUYyYVMxdVpYY3RjR0Z5ZEhrNk9qRXlNakE1TW1VM1pETXpZV014TUdNd1pqTmtOVFU1TnpZek5ESm1NemMxTlRWa1pqQTFaR0UxWWpjME1qazFObVExTm1FMk1tRmxNak0yTnpjMk9UQTNPV1F5T2tzeE1qSXdPRG82TVRJeU1EZ3pNRGd5WlRsaFpqRTFObVpsWVdWaU4yRm1aRE0yTTJFd1pXVTFabVpoTVdaa01UWXdPVFEzWWpZME4yRXhNemxoTjJVd1l6SmxaRGM0WmpWa1l6YzZWSEpoZG1rdGJtVjNMWEJoY25SNU9qb3hNakl3T1RKbE4yUXpNMkZqTVRCak1HWXpaRFUxT1RjMk16UXlaak0zTlRVMVpHWXdOV1JoTldJM05ESTVOVFprTlRaaE5qSmhaVEl6TmpjM05qa3dOemxrTXNBK3R0UDZwUC83bEFQU1B1UUVDZ015TGpFUzNBUUtSUUJReGlNdE9lWFI1TmtXc3R2cDA5WkszblMwTFp1VUtKRm9zVGhxVmoybGtjb1NFaUJUSUhxcTIvdW82a2xQbGVyTEVRakZyM2J2dEptSmNmTEp6WFpaVmxkd2hCSVBkSEpoWkdWM1pXSXRaR0ZrTFhZeEdua0tRRGd5WXpFd056VmhNekEzTlRGbE9URXpZVEJsTnpjeE0yTTBNRFZsTm1aak9XVmpaR0l6Tm1Fd01USTFORFZsTVdZNFlqTXhNREEwTURkaVlqUTRaVE1TQWxSWEVnaFBjR1Z5WVhSdmNoSUNWakVTQmtOdmMybG5iaG9iUTI5emFXZHVhVzVuUkdWc1pXZGhkR2x2YmxCeWIzQnZjMkZzSXE0QmFxc0JDbGdLVmpwVWNtRjJhUzF1WlhjdGNHRnlkSGs2T2pFeU1qQTVNbVUzWkRNellXTXhNR013WmpOa05UVTVOell6TkRKbU16YzFOVFZrWmpBMVpHRTFZamMwTWprMU5tUTFObUUyTW1GbE1qTTJOemMyT1RBM09XUXlDazhLVFRwTE1USXlNRGc2T2pFeU1qQTRNekE0TW1VNVlXWXhOVFptWldGbFlqZGhabVF6TmpOaE1HVmxOV1ptWVRGbVpERTJNRGswTjJJMk5EZGhNVE01WVRkbE1HTXlaV1EzT0dZMVpHTTNLbFJ5WVhacExXNWxkeTF3WVhKMGVUbzZNVEl5TURreVpUZGtNek5oWXpFd1l6Qm1NMlExTlRrM05qTTBNbVl6TnpVMU5XUm1NRFZrWVRWaU56UXlPVFUyWkRVMllUWXlZV1V5TXpZM056WTVNRGM1WkRJeVN6RXlNakE0T2pveE1qSXdPRE13T0RKbE9XRm1NVFUyWm1WaFpXSTNZV1prTXpZellUQmxaVFZtWm1FeFptUXhOakE1TkRkaU5qUTNZVEV6T1dFM1pUQmpNbVZrTnpobU5XUmpOem0ycVo3MDMxTUdBRUlxQ2lZS0pBZ0JFaUJEUzE4NWpwVmFHRm1KMnZYNTRuRjNadFRXVFhpU1VGWEwvZEw1RVNYTmxoQWUiLCJwcmVwYXJlZFRyYW5zYWN0aW9uSGFzaCI6ImQ2aVRkK1ZqWUEvY0p3N3BqK0NhS1ZTczBNcU1YVjlMWk5VNDVJajExODg9IiwiaGFzaGluZ1NjaGVtZVZlcnNpb24iOiJIQVNISU5HX1NDSEVNRV9WRVJTSU9OX1YyIiwiaGFzaGluZ0RldGFpbHMiOm51bGx9LCJ0eFR5cGUiOiJDb3NpZ25EZWxlZ2F0aW9uQWNjZXB0IiwicHJlcGFyZWRUcmFuc2FjdGlvbiI6IiIsInBhcnR5U2lnbmF0dXJlcyI6eyJzaWduYXR1cmVzIjpbXX0sImRlZHVwbGljYXRpb25QZXJpb2QiOnsiRW1wdHkiOnt9fSwiaGFzaGluZ1NjaGVtZVZlcnNpb24iOiJIQVNISU5HX1NDSEVNRV9WRVJTSU9OX1YyIiwibWluTGVkZ2VyVGltZSI6eyJ0aW1lIjp7IkVtcHR5Ijp7fX19LCJzdWJtaXNzaW9uSWQiOiIzOTM1YTA2ZC0zYjAzLTQxYmUtOTlhNS05NWIyZWNhYWJmN2QifQ=='; diff --git a/modules/sdk-coin-canton/test/unit/builder/cosignDelegationAccept/cosignDelegationAcceptBuilder.ts b/modules/sdk-coin-canton/test/unit/builder/cosignDelegationAccept/cosignDelegationAcceptBuilder.ts index dacc45e32c..08ec8b8c4e 100644 --- a/modules/sdk-coin-canton/test/unit/builder/cosignDelegationAccept/cosignDelegationAcceptBuilder.ts +++ b/modules/sdk-coin-canton/test/unit/builder/cosignDelegationAccept/cosignDelegationAcceptBuilder.ts @@ -3,8 +3,9 @@ import should from 'should'; import { coins } from '@bitgo/statics'; -import { CosignDelegationAcceptBuilder, Transaction } from '../../../../src'; -import { CantonTransferAcceptRejectRequest } from '../../../../src/lib/iface'; +import { CosignDelegationAcceptBuilder, Transaction, TransactionBuilderFactory } from '../../../../src'; +import { CantonTransferAcceptRejectRequest, TxData } from '../../../../src/lib/iface'; +import { CosignDelegationAcceptPrepareResponse, CosignDelegationAcceptRawTransaction } from '../../../resources'; const commandId = '3935a06d-3b03-41be-99a5-95b2ecaabf7d'; const contractId = @@ -79,4 +80,41 @@ describe('CosignDelegationAccept Builder', () => { txBuilder.initBuilder(tx); assert.throws(() => txBuilder.actAs(''), /actAsPartyId must be a non-empty string/); }); + + it('should parse preparedTransaction and extract sender, receiver, and amount via setTransaction + toJson', function () { + const txBuilder = new CosignDelegationAcceptBuilder(coins.get('tcanton')); + const tx = new Transaction(coins.get('tcanton')); + txBuilder.initBuilder(tx); + txBuilder.setTransaction(CosignDelegationAcceptPrepareResponse); + txBuilder.commandId(commandId).contractId(contractId).actAs(actAsPartyId); + const txData = txBuilder.transaction.toJson() as TxData; + should.exist(txData); + assert.equal(txData.sender, '12208::122083082e9af156feaeb7afd363a0ee5ffa1fd160947b647a139a7e0c2ed78f5dc7'); + assert.equal( + txData.receiver, + 'ravi-new-party::122092e7d33ac10c0f3d55976342f37555df05da5b742956d56a62ae2367769079d2' + ); + assert.equal(txData.amount, '0'); + }); + + it('should parse round-trip from raw transaction via TransactionBuilderFactory.from', function () { + const factory = new TransactionBuilderFactory(coins.get('tcanton')); + const txBuilder = factory.from(CosignDelegationAcceptRawTransaction); + const txData = txBuilder.transaction.toJson() as TxData; + should.exist(txData); + assert.equal(txData.sender, '12208::122083082e9af156feaeb7afd363a0ee5ffa1fd160947b647a139a7e0c2ed78f5dc7'); + assert.equal( + txData.receiver, + 'ravi-new-party::122092e7d33ac10c0f3d55976342f37555df05da5b742956d56a62ae2367769079d2' + ); + assert.equal(txData.amount, '0'); + }); + + it('should validate raw transaction hash', function () { + const txBuilder = new CosignDelegationAcceptBuilder(coins.get('tcanton')); + const tx = new Transaction(coins.get('tcanton')); + txBuilder.initBuilder(tx); + txBuilder.setTransaction(CosignDelegationAcceptPrepareResponse); + txBuilder.validateRawTransaction(CosignDelegationAcceptPrepareResponse.preparedTransaction); + }); }); diff --git a/modules/sdk-core/src/bitgo/utils/mpcUtils.ts b/modules/sdk-core/src/bitgo/utils/mpcUtils.ts index e328512199..6e8a610326 100644 --- a/modules/sdk-core/src/bitgo/utils/mpcUtils.ts +++ b/modules/sdk-core/src/bitgo/utils/mpcUtils.ts @@ -138,11 +138,19 @@ export abstract class MpcUtils { ); } - if (['transferAccept', 'transferReject'].includes(params.intentType) && baseCoin.getFamily() === 'canton') { + if ( + ['transferAccept', 'transferReject', 'cosignDelegationAccept', 'allocationAllocate'].includes( + params.intentType + ) && + baseCoin.getFamily() === 'canton' + ) { assert(params.txRequestId, `'txRequestId' is required parameter for ${params.intentType} intent`); } - if (params.intentType === 'transferOfferWithdrawn' && baseCoin.getFamily() === 'canton') { + if ( + ['transferOfferWithdrawn', 'allocationAllocateWithdrawn'].includes(params.intentType) && + baseCoin.getFamily() === 'canton' + ) { assert(params.transferOfferId, `'transferOfferId' is required parameter for ${params.intentType} intent`); } @@ -181,6 +189,9 @@ export abstract class MpcUtils { 'transferAccept', 'transferReject', 'transferOfferWithdrawn', + 'cosignDelegationAccept', + 'allocationAllocate', + 'allocationAllocateWithdrawn', 'bridgeFunds', 'cantonCommand', 'defi-approve', diff --git a/modules/sdk-core/src/bitgo/wallet/wallet.ts b/modules/sdk-core/src/bitgo/wallet/wallet.ts index c466ce8b7f..4d43c53b77 100644 --- a/modules/sdk-core/src/bitgo/wallet/wallet.ts +++ b/modules/sdk-core/src/bitgo/wallet/wallet.ts @@ -4308,6 +4308,32 @@ export class Wallet implements IWallet { ); break; } + case 'cosignDelegationAccept': { + txRequest = await this.tssUtils!.prebuildTxWithIntent( + { + reqId, + intentType: 'cosignDelegationAccept', + txRequestId: params.txRequestId, + sequenceId: params.txRequestId, + }, + apiVersion, + params.preview + ); + break; + } + case 'allocationAllocate': { + txRequest = await this.tssUtils!.prebuildTxWithIntent( + { + reqId, + intentType: 'allocationAllocate', + txRequestId: params.txRequestId, + sequenceId: params.txRequestId, + }, + apiVersion, + params.preview + ); + break; + } case 'transferReject': { txRequest = await this.tssUtils!.prebuildTxWithIntent( { @@ -4334,6 +4360,19 @@ export class Wallet implements IWallet { ); break; } + case 'allocationAllocateWithdrawn': { + txRequest = await this.tssUtils!.prebuildTxWithIntent( + { + reqId, + intentType: 'allocationAllocateWithdrawn', + transferOfferId: params.transferOfferId, + sequenceId: params.transferOfferId, + }, + apiVersion, + params.preview + ); + break; + } case 'cantonCommand': { if (!params.cantonCommandParams) { throw new Error('cantonCommandParams is required for cantonCommand intent'); From 296661e3932554fc7c7adfecf2f73d5a446e6524 Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Fri, 22 May 2026 13:54:30 +0200 Subject: [PATCH 14/34] refactor(abstract-utxo): delete fetchInputs and remove redundant fee guard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two related cleanups enabled by dead-code removal in the parent PR: 1. **Delete fetchInputs.ts** — getTxInputs and getPsbtTxInputs are no longer called anywhere after the parent PR's dead-code removal. 2. **Remove fee guard from verifyTransaction** — the `inputAmount - outputAmount < 0` check in fixedScript/verifyTransaction.ts is unreachable dead code. The PSBT path calls psbt.parseTransactionWithWalletKeys (wasm), whose Rust implementation uses checked_sub to compute miner_fee and returns ParseTransactionError::FeeCalculation ("Fee calculation error: outputs exceed inputs") if total output value exceeds total input value. This fires before the TypeScript guard would ever run. A new unit test in test/unit/transaction/fixedScript/explainPsbt.ts verifies this: it tampers a PSBT's witnessUtxo.value to make outputs > inputs and asserts that explainPsbtWasmBigInt throws with that message. Refs: T1-3279 --- .../src/transaction/fetchInputs.ts | 86 ------------------- .../fixedScript/verifyTransaction.ts | 24 +----- .../abstract-utxo/src/transaction/index.ts | 1 - .../transaction/fixedScript/explainPsbt.ts | 27 ++++++ 4 files changed, 28 insertions(+), 110 deletions(-) delete mode 100644 modules/abstract-utxo/src/transaction/fetchInputs.ts diff --git a/modules/abstract-utxo/src/transaction/fetchInputs.ts b/modules/abstract-utxo/src/transaction/fetchInputs.ts deleted file mode 100644 index 01459430a4..0000000000 --- a/modules/abstract-utxo/src/transaction/fetchInputs.ts +++ /dev/null @@ -1,86 +0,0 @@ -import * as utxolib from '@bitgo/utxo-lib'; -import { BitGoBase, IRequestTracer } from '@bitgo/sdk-core'; - -import { AbstractUtxoCoin, TransactionPrebuild } from '../abstractUtxoCoin'; -import { getNetworkFromCoinName, UtxoCoinName } from '../names'; - -/** - * Get the inputs for a psbt from a prebuild. - */ -export function getPsbtTxInputs( - psbtArg: string | utxolib.bitgo.UtxoPsbt, - coinName: UtxoCoinName -): { address: string; value: bigint; valueString: string }[] { - const network = getNetworkFromCoinName(coinName); - const psbt = psbtArg instanceof utxolib.bitgo.UtxoPsbt ? psbtArg : utxolib.bitgo.createPsbtFromHex(psbtArg, network); - const txInputs = psbt.txInputs; - return psbt.data.inputs.map((input, index) => { - let address: string; - let value: bigint; - if (input.witnessUtxo) { - address = utxolib.address.fromOutputScript(input.witnessUtxo.script, network); - value = input.witnessUtxo.value; - } else if (input.nonWitnessUtxo) { - const tx = utxolib.bitgo.createTransactionFromBuffer(input.nonWitnessUtxo, network, { - amountType: 'bigint', - }); - const txId = (Buffer.from(txInputs[index].hash).reverse() as Buffer).toString('hex'); - if (tx.getId() !== txId) { - throw new Error('input transaction hex does not match id'); - } - const prevTxOutputIndex = txInputs[index].index; - address = utxolib.address.fromOutputScript(tx.outs[prevTxOutputIndex].script, network); - value = tx.outs[prevTxOutputIndex].value; - } else { - throw new Error('psbt input is missing both witnessUtxo and nonWitnessUtxo'); - } - return { address, value, valueString: value.toString() }; - }); -} - -/** - * Get the inputs for a transaction from a prebuild. - */ -export async function getTxInputs(params: { - txPrebuild: TransactionPrebuild; - bitgo: BitGoBase; - coin: AbstractUtxoCoin; - disableNetworking: boolean; - reqId?: IRequestTracer; -}): Promise<{ address: string; value: TNumber; valueString: string }[]> { - const { txPrebuild, bitgo, coin, disableNetworking, reqId } = params; - if (!txPrebuild.txHex) { - throw new Error(`txPrebuild.txHex not set`); - } - const transaction = coin.createTransactionFromHex(txPrebuild.txHex); - const transactionCache = {}; - return await Promise.all( - transaction.ins.map(async (currentInput): Promise<{ address: string; value: TNumber; valueString: string }> => { - const transactionId = (Buffer.from(currentInput.hash).reverse() as Buffer).toString('hex'); - const txHex = txPrebuild.txInfo?.txHexes?.[transactionId]; - if (txHex) { - const localTx = coin.createTransactionFromHex(txHex); - if (localTx.getId() !== transactionId) { - throw new Error('input transaction hex does not match id'); - } - const currentOutput = localTx.outs[currentInput.index]; - const address = utxolib.address.fromOutputScript(currentOutput.script, coin.network); - return { - address, - value: currentOutput.value, - valueString: currentOutput.value.toString(), - }; - } else if (!transactionCache[transactionId]) { - if (disableNetworking) { - throw new Error('attempting to retrieve transaction details externally with networking disabled'); - } - if (reqId) { - bitgo.setRequestTracer(reqId); - } - transactionCache[transactionId] = await bitgo.get(coin.url(`/public/tx/${transactionId}`)).result(); - } - const transactionDetails = transactionCache[transactionId]; - return transactionDetails.outputs[currentInput.index]; - }) - ); -} diff --git a/modules/abstract-utxo/src/transaction/fixedScript/verifyTransaction.ts b/modules/abstract-utxo/src/transaction/fixedScript/verifyTransaction.ts index 9f4aba34a9..eff4f13df3 100644 --- a/modules/abstract-utxo/src/transaction/fixedScript/verifyTransaction.ts +++ b/modules/abstract-utxo/src/transaction/fixedScript/verifyTransaction.ts @@ -5,11 +5,9 @@ import { BitGoBase, TxIntentMismatchError, IBaseCoin } from '@bitgo/sdk-core'; import { hasPsbtMagic } from '@bitgo/wasm-utxo'; import { AbstractUtxoCoin, VerifyTransactionOptions } from '../../abstractUtxoCoin'; -import { Output, ParsedTransaction } from '../types'; -import { toTNumber } from '../../tnumber'; +import { ParsedTransaction } from '../types'; import { stringToBufferTryFormats } from '../decode'; import { verifyCustomChangeKeySignatures, verifyKeySignature, verifyUserPublicKeyAsync } from '../../verifyKey'; -import { getPsbtTxInputs, getTxInputs } from '../fetchInputs'; const debug = buildDebug('bitgo:abstract-utxo:verifyTransaction'); @@ -201,29 +199,9 @@ export async function verifyTransaction( } } - const allOutputs = parsedTransaction.outputs; if (!txPrebuild.txHex) { throw new Error(`txPrebuild.txHex not set`); } - const inputs = isPsbt - ? getPsbtTxInputs(txPrebuild.txHex, coin.name).map((v) => ({ - ...v, - value: toTNumber(v.value, coin.amountType), - })) - : await getTxInputs({ txPrebuild, bitgo, coin, disableNetworking, reqId }); - // coins (doge) that can exceed number limits (and thus will use bigint) will have the `valueString` field - const inputAmount = inputs.reduce( - (sum: bigint, i) => sum + BigInt(coin.amountType === 'bigint' ? i.valueString : i.value), - BigInt(0) - ); - const outputAmount = allOutputs.reduce((sum: bigint, o: Output) => sum + BigInt(o.amount), BigInt(0)); - const fee = inputAmount - outputAmount; - - if (fee < 0) { - throw new Error( - `attempting to spend ${outputAmount} satoshis, which exceeds the input amount (${inputAmount} satoshis) by ${-fee}` - ); - } return true; } diff --git a/modules/abstract-utxo/src/transaction/index.ts b/modules/abstract-utxo/src/transaction/index.ts index 075ef05742..d0231ff6cb 100644 --- a/modules/abstract-utxo/src/transaction/index.ts +++ b/modules/abstract-utxo/src/transaction/index.ts @@ -3,7 +3,6 @@ export * from './recipient'; export { explainTx } from './explainTransaction'; export { parseTransaction } from './parseTransaction'; export { verifyTransaction } from './verifyTransaction'; -export * from './fetchInputs'; export * as bip322 from './bip322'; export { decodePsbt } from './decode'; export * from './fixedScript'; diff --git a/modules/abstract-utxo/test/unit/transaction/fixedScript/explainPsbt.ts b/modules/abstract-utxo/test/unit/transaction/fixedScript/explainPsbt.ts index 1d7e29d091..2cc561e871 100644 --- a/modules/abstract-utxo/test/unit/transaction/fixedScript/explainPsbt.ts +++ b/modules/abstract-utxo/test/unit/transaction/fixedScript/explainPsbt.ts @@ -94,6 +94,33 @@ function describeTransactionWith(acidTest: testutil.AcidTest) { describe('explainPsbt(Wasm)', function () { testutil.AcidTest.suite().forEach((test) => describeTransactionWith(test)); + + it('explainPsbtWasmBigInt throws when total output value exceeds total input value', function () { + const network = utxolib.networks.bitcoin; + const rootWalletKeys = testutil.getDefaultWalletKeys(); + // Valid PSBT: one p2wsh input (5000 sat), one internal p2wsh output (3000 sat), fee = 2000 + const psbt = testutil.constructPsbt( + [{ scriptType: 'p2wsh', value: 5000n }], + [{ scriptType: 'p2wsh', value: 3000n, isInternalAddress: true }], + network, + rootWalletKeys, + 'unsigned' + ); + // Tamper: reduce the witnessUtxo.value so that inputs (1000) < outputs (3000). + // Direct mutation avoids bip174's "duplicate data" guard on updateInput. + psbt.data.inputs[0].witnessUtxo!.value = 1000n; + + const wasmPsbt = fixedScriptWallet.BitGoPsbt.fromBytes(psbt.toBuffer(), 'bitcoin'); + const walletXpubs = fixedScriptWallet.RootWalletKeys.from(rootWalletKeys); + + assert.throws( + () => + explainPsbtWasmBigInt(wasmPsbt, walletXpubs, { + replayProtection: { publicKeys: [] }, + }), + /Fee calculation error: outputs exceed inputs/ + ); + }); }); describe('aggregateTransactionExplanations', function () { From 40e7e91b5dbb7a6760510c4c49d2f836d3166c05 Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Fri, 22 May 2026 14:11:03 +0200 Subject: [PATCH 15/34] refactor(abstract-utxo): remove createTransactionFromHex from AbstractUtxoCoin The only remaining caller was the deleted fetchInputs and the doge override that just delegated to super. Remove the method, drop the doge override, and remove the now-unused createTransactionFromHex stubs from verifyTransaction tests. Refs: T1-3279 --- modules/abstract-utxo/src/abstractUtxoCoin.ts | 6 ----- modules/abstract-utxo/src/impl/doge/doge.ts | 5 ---- .../src/recovery/backupKeyRecovery.ts | 6 ++--- .../test/unit/verifyTransaction.ts | 26 ------------------- 4 files changed, 3 insertions(+), 40 deletions(-) diff --git a/modules/abstract-utxo/src/abstractUtxoCoin.ts b/modules/abstract-utxo/src/abstractUtxoCoin.ts index 6eaaf840cf..172406990f 100644 --- a/modules/abstract-utxo/src/abstractUtxoCoin.ts +++ b/modules/abstract-utxo/src/abstractUtxoCoin.ts @@ -599,12 +599,6 @@ export abstract class AbstractUtxoCoin extends BaseCoin implements Musig2Partici return isChainCode(addressDetails.chain) ? scriptTypeForChain(addressDetails.chain) : null; } - createTransactionFromHex( - hex: string - ): utxolib.bitgo.UtxoTransaction { - return utxolib.bitgo.createTransactionFromHex(hex, this.network, this.amountType); - } - decodeTransaction(input: Buffer | string): fixedScriptWallet.BitGoPsbt { const buffer = typeof input === 'string' ? stringToBufferTryFormats(input, ['hex', 'base64']) : input; if (!hasPsbtMagic(buffer)) { diff --git a/modules/abstract-utxo/src/impl/doge/doge.ts b/modules/abstract-utxo/src/impl/doge/doge.ts index 79fe2da15c..f1ba54f030 100644 --- a/modules/abstract-utxo/src/impl/doge/doge.ts +++ b/modules/abstract-utxo/src/impl/doge/doge.ts @@ -1,5 +1,4 @@ import { BitGoBase, HalfSignedUtxoTransaction, SignedTransaction } from '@bitgo/sdk-core'; -import { bitgo } from '@bitgo/utxo-lib'; import { AbstractUtxoCoin, @@ -75,10 +74,6 @@ export class Doge extends AbstractUtxoCoin { /* postProcessPrebuild, isBitGoTaintedUnspent, verifyCustomChangeKeySignatures do not care whether they receive number or bigint */ - createTransactionFromHex(hex: string): bitgo.UtxoTransaction { - return super.createTransactionFromHex(hex); - } - async parseTransaction( params: ParseTransactionOptions ): /* diff --git a/modules/abstract-utxo/src/recovery/backupKeyRecovery.ts b/modules/abstract-utxo/src/recovery/backupKeyRecovery.ts index 3cf4c50052..d041049896 100644 --- a/modules/abstract-utxo/src/recovery/backupKeyRecovery.ts +++ b/modules/abstract-utxo/src/recovery/backupKeyRecovery.ts @@ -8,7 +8,7 @@ import { krsProviders, Triple, } from '@bitgo/sdk-core'; -import { BIP32, fixedScriptWallet } from '@bitgo/wasm-utxo'; +import { BIP32, fixedScriptWallet, Transaction } from '@bitgo/wasm-utxo'; import { AbstractUtxoCoin } from '../abstractUtxoCoin'; import { signAndVerifyPsbt } from '../transaction/fixedScript/signTransaction'; @@ -172,8 +172,8 @@ async function queryBlockchainUnspentsPath( // json parse won't parse it correctly, so we requery the txid for the tx hex to decode here if (!Number.isSafeInteger(u.value)) { const txHex = await getPrevTx(txid); - const tx = coin.createTransactionFromHex(txHex); - val = tx.outs[vout].value; + const tx = Transaction.fromBytes(Buffer.from(txHex, 'hex')); + val = tx.getOutputs()[vout].value; } } // the api may return cashaddr's instead of legacy for BCH and BCHA diff --git a/modules/abstract-utxo/test/unit/verifyTransaction.ts b/modules/abstract-utxo/test/unit/verifyTransaction.ts index 35b58a9568..70c0ef9946 100644 --- a/modules/abstract-utxo/test/unit/verifyTransaction.ts +++ b/modules/abstract-utxo/test/unit/verifyTransaction.ts @@ -1,6 +1,5 @@ import assert from 'assert'; -import * as utxolib from '@bitgo/utxo-lib'; import * as sinon from 'sinon'; import { Wallet } from '@bitgo/sdk-core'; @@ -217,10 +216,6 @@ describe('Verify Transaction', function () { needsCustomChangeKeySignatureVerification: false, }); - const bitcoinMock = sinon - .stub(coin, 'createTransactionFromHex') - .returns({ ins: [] } as unknown as utxolib.bitgo.UtxoTransaction); - const result = await coin.verifyTransaction({ txParams: { walletPassphrase: passphrase, @@ -234,7 +229,6 @@ describe('Verify Transaction', function () { assert.strictEqual(result, true); coinMock.restore(); - bitcoinMock.restore(); }); it('should not allow any implicit external outputs if paygo outputs are disallowed', async () => { @@ -284,10 +278,6 @@ describe('Verify Transaction', function () { needsCustomChangeKeySignatureVerification: false, }); - const bitcoinMock = sinon - .stub(coin, 'createTransactionFromHex') - .returns({ ins: [] } as unknown as utxolib.bitgo.UtxoTransaction); - const result = await coin.verifyTransaction({ txParams: { walletPassphrase: passphrase, @@ -302,7 +292,6 @@ describe('Verify Transaction', function () { assert.strictEqual(result, true); coinMock.restore(); - bitcoinMock.restore(); }); it('should verify a bridging transaction whose implicit external output matches the bridge amount', async () => { @@ -329,10 +318,6 @@ describe('Verify Transaction', function () { needsCustomChangeKeySignatureVerification: false, }); - const bitcoinMock = sinon - .stub(coin, 'createTransactionFromHex') - .returns({ ins: [] } as unknown as utxolib.bitgo.UtxoTransaction); - const result = await coin.verifyTransaction({ txParams: { walletPassphrase: passphrase, @@ -349,7 +334,6 @@ describe('Verify Transaction', function () { assert.strictEqual(result, true); coinMock.restore(); - bitcoinMock.restore(); }); it('should reject a bridging transaction whose implicit external output does not match the bridge amount', async () => { @@ -560,10 +544,6 @@ describe('Verify Transaction', function () { needsCustomChangeKeySignatureVerification: false, }); - const bitcoinMock = sinon - .stub(coin, 'createTransactionFromHex') - .returns({ ins: [] } as unknown as utxolib.bitgo.UtxoTransaction); - const result = await coin.verifyTransaction({ txParams: { walletPassphrase: passphrase }, txPrebuild: { txHex: '00' }, @@ -574,7 +554,6 @@ describe('Verify Transaction', function () { assert.strictEqual(result, true); coinMock.restore(); - bitcoinMock.restore(); }); }); @@ -605,10 +584,6 @@ describe('Verify Transaction', function () { needsCustomChangeKeySignatureVerification: false, }); - const bitcoinMock = sinon - .stub(bigintCoin, 'createTransactionFromHex') - .returns({ ins: [] } as unknown as utxolib.bitgo.UtxoTransaction); - const result = await bigintCoin.verifyTransaction({ txParams: { walletPassphrase: passphrase, @@ -623,6 +598,5 @@ describe('Verify Transaction', function () { assert.strictEqual(result, true); coinMock.restore(); - bitcoinMock.restore(); }); }); From eb3d6819a3a86e1c2cc6edd393bab83901cecf87 Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Fri, 22 May 2026 14:20:57 +0200 Subject: [PATCH 16/34] refactor(abstract-utxo): pass coinName directly to BitGoPsbt.fromBytes BitGoPsbt.fromBytes accepts NetworkName = UtxolibName | CoinName, so the round-trip through utxolib.getNetworkName is unnecessary. Drop the utxolib import from decode.ts. Refs: T1-3279 --- modules/abstract-utxo/src/transaction/decode.ts | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/modules/abstract-utxo/src/transaction/decode.ts b/modules/abstract-utxo/src/transaction/decode.ts index 556e5e73d4..35f4337582 100644 --- a/modules/abstract-utxo/src/transaction/decode.ts +++ b/modules/abstract-utxo/src/transaction/decode.ts @@ -1,7 +1,6 @@ -import * as utxolib from '@bitgo/utxo-lib'; -import { fixedScriptWallet, hasPsbtMagic, Psbt as WasmPsbt, utxolibCompat } from '@bitgo/wasm-utxo'; +import { fixedScriptWallet, hasPsbtMagic, Psbt as WasmPsbt } from '@bitgo/wasm-utxo'; -import { getNetworkFromCoinName, UtxoCoinName } from '../names'; +import { UtxoCoinName } from '../names'; import { BitGoPsbt } from './types'; @@ -22,20 +21,11 @@ export function stringToBufferTryFormats(input: string, formats: BufferEncoding[ throw new Error('input must be a valid hex or base64 string'); } -function toNetworkName(coinName: UtxoCoinName): utxolibCompat.UtxolibName { - const network = getNetworkFromCoinName(coinName); - const networkName = utxolib.getNetworkName(network); - if (!networkName) { - throw new Error(`Invalid coinName: ${coinName}`); - } - return networkName; -} - export function decodePsbt(psbt: string | Buffer, coinName: UtxoCoinName): BitGoPsbt { if (typeof psbt === 'string') { psbt = Buffer.from(psbt, 'hex'); } - return fixedScriptWallet.BitGoPsbt.fromBytes(psbt, toNetworkName(coinName)); + return fixedScriptWallet.BitGoPsbt.fromBytes(psbt, coinName); } export type PrebuildLike = { From 03885ccdfbec8a65a7a838a58d61ed814048cce1 Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Fri, 22 May 2026 14:38:48 +0200 Subject: [PATCH 17/34] refactor(abstract-utxo): replace 3 utxolib network/script helpers with wasm/coinName equivalents - supportsBlockTarget: compare coinName instead of utxolib network objects - isSupportedScriptType: use fixedScriptWallet.supportsScriptType from wasm-utxo - postProcessPrebuild: delete the psbt-lite detection block and the allowNonSegwitSigningWithoutPrevTx flag end-to-end (wasm-utxo signing is permissive by default and never read the flag) Refs: T1-3279 --- modules/abstract-utxo/src/abstractUtxoCoin.ts | 29 +++---------------- .../fixedScript/signTransaction.ts | 2 -- .../src/transaction/signTransaction.ts | 1 - 3 files changed, 4 insertions(+), 28 deletions(-) diff --git a/modules/abstract-utxo/src/abstractUtxoCoin.ts b/modules/abstract-utxo/src/abstractUtxoCoin.ts index 172406990f..f2b23638da 100644 --- a/modules/abstract-utxo/src/abstractUtxoCoin.ts +++ b/modules/abstract-utxo/src/abstractUtxoCoin.ts @@ -4,7 +4,7 @@ import { randomBytes } from 'crypto'; import _ from 'lodash'; import * as utxolib from '@bitgo/utxo-lib'; import { BIP32, fixedScriptWallet, hasPsbtMagic } from '@bitgo/wasm-utxo'; -import { bitgo, getMainnet } from '@bitgo/utxo-lib'; +import { bitgo } from '@bitgo/utxo-lib'; import { AddressCoinSpecific, BaseCoin, @@ -335,11 +335,6 @@ type UtxoBaseSignTransactionOptions = * When false, creates half-signed transaction with placeholder signatures. */ isLastSignature?: boolean; - /** - * If true, allows signing a non-segwit input with a witnessUtxo instead requiring a previous - * transaction (nonWitnessUtxo) - */ - allowNonSegwitSigningWithoutPrevTx?: boolean; /** * When true, the signed transaction will be converted from PSBT to legacy format before returning. * Set automatically by presignTransaction() when the caller explicitly requested txFormat: 'legacy'. @@ -455,13 +450,8 @@ export abstract class AbstractUtxoCoin extends BaseCoin implements Musig2Partici /** Indicates whether the coin supports a block target */ supportsBlockTarget(): boolean { // FIXME: the SDK does not seem to use this anywhere so it is unclear what the purpose of this method is - switch (getMainnet(this.network)) { - case utxolib.networks.bitcoin: - case utxolib.networks.dogecoin: - return true; - default: - return false; - } + const mainnet = getMainnetCoinName(this.name); + return mainnet === 'btc' || mainnet === 'doge'; } sweepWithSendMany(): boolean { @@ -755,7 +745,7 @@ export abstract class AbstractUtxoCoin extends BaseCoin implements Musig2Partici * @returns true iff coin supports spending from unspentType */ supportsAddressType(addressType: ScriptType2Of3): boolean { - return utxolib.bitgo.outputScripts.isSupportedScriptType(this.network, addressType); + return fixedScriptWallet.supportsScriptType(this.name, addressType); } /** inherited doc */ @@ -1057,17 +1047,6 @@ export abstract class AbstractUtxoCoin extends BaseCoin implements Musig2Partici } const returnLegacyFormat = (params as Record).txFormat === 'legacy'; - - // In the case that we have a 'psbt-lite' transaction format, we want to indicate in signing to not fail - const txHex = (params.txHex ?? params.txPrebuild?.txHex) as string; - if ( - txHex && - utxolib.bitgo.isPsbt(txHex as string) && - utxolib.bitgo.isPsbtLite(utxolib.bitgo.createPsbtFromHex(txHex, this.network)) && - params.allowNonSegwitSigningWithoutPrevTx === undefined - ) { - return { ...params, allowNonSegwitSigningWithoutPrevTx: true, returnLegacyFormat }; - } return { ...params, returnLegacyFormat }; } diff --git a/modules/abstract-utxo/src/transaction/fixedScript/signTransaction.ts b/modules/abstract-utxo/src/transaction/fixedScript/signTransaction.ts index 49c333c4f0..a9dec08532 100644 --- a/modules/abstract-utxo/src/transaction/fixedScript/signTransaction.ts +++ b/modules/abstract-utxo/src/transaction/fixedScript/signTransaction.ts @@ -33,8 +33,6 @@ export async function signTransaction( txInfo: { unspents?: Unspent[] } | undefined; isLastSignature: boolean; signingStep: 'signerNonce' | 'cosignerNonce' | 'signerSignature' | undefined; - /** deprecated */ - allowNonSegwitSigningWithoutPrevTx: boolean; pubs: string[] | undefined; cosignerPub: string | undefined; /** When true (default), extract finalized PSBT to legacy transaction format. When false, return finalized PSBT. */ diff --git a/modules/abstract-utxo/src/transaction/signTransaction.ts b/modules/abstract-utxo/src/transaction/signTransaction.ts index 23f27de260..7a08d0b79f 100644 --- a/modules/abstract-utxo/src/transaction/signTransaction.ts +++ b/modules/abstract-utxo/src/transaction/signTransaction.ts @@ -65,7 +65,6 @@ export async function signTransaction( txInfo: params.txPrebuild.txInfo, isLastSignature: params.isLastSignature ?? false, signingStep: params.signingStep, - allowNonSegwitSigningWithoutPrevTx: params.allowNonSegwitSigningWithoutPrevTx ?? false, pubs: params.pubs, cosignerPub: params.cosignerPub, extractTransaction: params.extractTransaction, From 1ac21ec854e91c7dc7d44c48ba2f2c709a06c491 Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Fri, 22 May 2026 14:43:35 +0200 Subject: [PATCH 18/34] refactor(abstract-utxo): migrate isValidAddress off utxolib.addressFormat Replace utxolib.addressFormat.toOutputScriptAndFormat / fromOutputScriptWithFormat with wasm-utxo's address.toOutputScriptWithCoin / fromOutputScriptWithCoin. Detect address format by trying each candidate and checking which round-trips byte-equal. Refs: T1-3279 --- modules/abstract-utxo/src/abstractUtxoCoin.ts | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/modules/abstract-utxo/src/abstractUtxoCoin.ts b/modules/abstract-utxo/src/abstractUtxoCoin.ts index f2b23638da..778234d7e7 100644 --- a/modules/abstract-utxo/src/abstractUtxoCoin.ts +++ b/modules/abstract-utxo/src/abstractUtxoCoin.ts @@ -3,7 +3,7 @@ import { randomBytes } from 'crypto'; import _ from 'lodash'; import * as utxolib from '@bitgo/utxo-lib'; -import { BIP32, fixedScriptWallet, hasPsbtMagic } from '@bitgo/wasm-utxo'; +import { address as wasmAddress, BIP32, fixedScriptWallet, hasPsbtMagic } from '@bitgo/wasm-utxo'; import { bitgo } from '@bitgo/utxo-lib'; import { AddressCoinSpecific, @@ -498,15 +498,20 @@ export abstract class AbstractUtxoCoin extends BaseCoin implements Musig2Partici // At the time of writing, the only additional address format is bch cashaddr. const anyFormat = (param as { anyFormat: boolean } | undefined)?.anyFormat ?? true; try { - // Find out if the address is valid for any format. Tries all supported formats by default. - // Throws if address cannot be decoded with any format. - const [format, script] = utxolib.addressFormat.toOutputScriptAndFormat(address, this.network); - // unless anyFormat is set, only 'default' is allowed. - if (!anyFormat && format !== 'default') { - return false; + const script = wasmAddress.toOutputScriptWithCoin(address, this.name); + // Determine which format the input address was in by round-tripping + // through each candidate and checking byte-equality. 'default' is tried + // first so canonical default-format addresses early-exit. + for (const format of ['default', 'cashaddr'] as const) { + try { + if (wasmAddress.fromOutputScriptWithCoin(script, this.name, format) === address) { + return anyFormat || format === 'default'; + } + } catch { + // coin doesn't support this format; try the next one + } } - // make sure that address is in normal representation for given format. - return address === utxolib.addressFormat.fromOutputScriptWithFormat(script, format, this.network); + return false; } catch (e) { return false; } From c66a6e9432edca25624bb522e697776a2fc5becc Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Fri, 22 May 2026 14:50:17 +0200 Subject: [PATCH 19/34] refactor(abstract-utxo): replace utxolib.bitgo helpers and drop dead branches - isChainCode / scriptTypeForChain -> ChainCode.is / ChainCode.scriptType (wasm-utxo) - hasKeyPathSpendInput: drop unreachable utxolib.bitgo.UtxoPsbt branch and isTransactionWithKeyPathSpendInput call (DecodedTransaction is now BitGoPsbt only) - validAddressTypes: inline the 2-of-3 list instead of utxolib.outputScripts.scriptTypes2Of3 - Drop unused RootWalletKeys / UtxoNetwork type re-exports and the now-empty utxolib bitgo import Three utxolib references remain in abstractUtxoCoin: the ScriptType2Of3 type re-export, the deprecated network getter (still used by tests), and the top-level utxolib import that backs both. Refs: T1-3279 --- modules/abstract-utxo/src/abstractUtxoCoin.ts | 40 ++++++++----------- 1 file changed, 16 insertions(+), 24 deletions(-) diff --git a/modules/abstract-utxo/src/abstractUtxoCoin.ts b/modules/abstract-utxo/src/abstractUtxoCoin.ts index 778234d7e7..ed534d417f 100644 --- a/modules/abstract-utxo/src/abstractUtxoCoin.ts +++ b/modules/abstract-utxo/src/abstractUtxoCoin.ts @@ -4,7 +4,6 @@ import { randomBytes } from 'crypto'; import _ from 'lodash'; import * as utxolib from '@bitgo/utxo-lib'; import { address as wasmAddress, BIP32, fixedScriptWallet, hasPsbtMagic } from '@bitgo/wasm-utxo'; -import { bitgo } from '@bitgo/utxo-lib'; import { AddressCoinSpecific, BaseCoin, @@ -62,7 +61,6 @@ import { getReplayProtectionPubkeys, isReplayProtectionUnspent } from './transac import { supportedCrossChainRecoveries } from './config'; import { assertValidTransactionRecipient, - DecodedTransaction, explainTx, fromExtendedAddressFormat, isScriptRecipient, @@ -142,28 +140,21 @@ type UtxoCustomSigningFunction = { }): Promise; }; -const { isChainCode, scriptTypeForChain, outputScripts } = bitgo; +const { ChainCode } = fixedScriptWallet; /** * Check if a decoded transaction has at least one taproot key path spend (MuSig2) input. - * Works for both utxolib UtxoPsbt and wasm-utxo BitGoPsbt. */ -function hasKeyPathSpendInput( - tx: DecodedTransaction, +function hasKeyPathSpendInput( + tx: fixedScriptWallet.BitGoPsbt, pubs: string[] | undefined, coinName: UtxoCoinName ): boolean { - if (tx instanceof bitgo.UtxoPsbt) { - return bitgo.isTransactionWithKeyPathSpendInput(tx); - } - if (tx instanceof fixedScriptWallet.BitGoPsbt) { - assert(pubs && isTriple(pubs), 'pub triple is required to check for key path spend inputs in wasm-utxo PSBT'); - const rootWalletKeys = fixedScriptWallet.RootWalletKeys.fromXpubs(pubs); - const replayProtection = { publicKeys: getReplayProtectionPubkeys(coinName) }; - const parsed = tx.parseTransactionWithWalletKeys(rootWalletKeys, { replayProtection }); - return parsed.inputs.some((input) => input.scriptType === 'p2trMusig2KeyPath'); - } - return false; + assert(pubs && isTriple(pubs), 'pub triple is required to check for key path spend inputs in wasm-utxo PSBT'); + const rootWalletKeys = fixedScriptWallet.RootWalletKeys.fromXpubs(pubs); + const replayProtection = { publicKeys: getReplayProtectionPubkeys(coinName) }; + const parsed = tx.parseTransactionWithWalletKeys(rootWalletKeys, { replayProtection }); + return parsed.inputs.some((input) => input.scriptType === 'p2trMusig2KeyPath'); } /** @@ -216,8 +207,6 @@ function convertValidationErrorToTxIntentMismatch( export type { DecodedTransaction } from './transaction/types'; -export type RootWalletKeys = bitgo.RootWalletKeys; - export type UtxoCoinSpecific = AddressCoinSpecific | DescriptorAddressCoinSpecific; export interface VerifyAddressOptions extends BaseVerifyAddressOptions { @@ -252,8 +241,6 @@ export interface DecoratedExplainTransactionOptions extends BaseTransactionPrebuild { txInfo?: TransactionInfo; blockHeight?: number; @@ -460,7 +447,7 @@ export abstract class AbstractUtxoCoin extends BaseCoin implements Musig2Partici /** @deprecated */ static get validAddressTypes(): ScriptType2Of3[] { - return [...outputScripts.scriptTypes2Of3]; + return ['p2sh', 'p2shP2wsh', 'p2wsh', 'p2tr', 'p2trMusig2']; } /** @@ -591,7 +578,9 @@ export abstract class AbstractUtxoCoin extends BaseCoin implements Musig2Partici * @param addressDetails */ static inferAddressType(addressDetails: { chain: number }): ScriptType2Of3 | null { - return isChainCode(addressDetails.chain) ? scriptTypeForChain(addressDetails.chain) : null; + return fixedScriptWallet.ChainCode.is(addressDetails.chain) + ? (fixedScriptWallet.ChainCode.scriptType(addressDetails.chain) as ScriptType2Of3) + : null; } decodeTransaction(input: Buffer | string): fixedScriptWallet.BitGoPsbt { @@ -763,7 +752,10 @@ export abstract class AbstractUtxoCoin extends BaseCoin implements Musig2Partici * @return true iff coin supports spending from chain */ supportsAddressChain(chain: number): boolean { - return isChainCode(chain) && this.supportsAddressType(utxolib.bitgo.scriptTypeForChain(chain)); + return ( + fixedScriptWallet.ChainCode.is(chain) && + this.supportsAddressType(fixedScriptWallet.ChainCode.scriptType(chain) as ScriptType2Of3) + ); } keyIdsForSigning(): number[] { From 9a81813e14713003a2cc02b17988a797415c624f Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Fri, 22 May 2026 15:01:54 +0200 Subject: [PATCH 20/34] refactor(abstract-utxo): remove deprecated network getter and getNetworkFromCoinName Drop the deprecated coin.network getter from AbstractUtxoCoin and the getNetworkFromCoinName helper from names.ts. Tests that previously called coin.network now call the test-local getNetworkForCoinName(coin.name) from util/utxoCoins. With this, names.ts no longer depends on utxolib. Refs: T1-3279 --- modules/abstract-utxo/src/abstractUtxoCoin.ts | 17 +-- modules/abstract-utxo/src/names.ts | 108 ------------------ .../test/unit/buildSignSendLegacyFormat.ts | 10 +- .../abstract-utxo/test/unit/customSigner.ts | 13 ++- .../test/unit/prebuildAndSign.ts | 14 ++- .../test/unit/signTransaction.ts | 33 ++++-- .../abstract-utxo/test/unit/transaction.ts | 23 ++-- .../unit/transaction/fixedScript/parsePsbt.ts | 5 +- modules/abstract-utxo/test/unit/wallet.ts | 6 +- 9 files changed, 73 insertions(+), 156 deletions(-) diff --git a/modules/abstract-utxo/src/abstractUtxoCoin.ts b/modules/abstract-utxo/src/abstractUtxoCoin.ts index ed534d417f..530fb9ffa2 100644 --- a/modules/abstract-utxo/src/abstractUtxoCoin.ts +++ b/modules/abstract-utxo/src/abstractUtxoCoin.ts @@ -75,14 +75,7 @@ import { ErrorImplicitExternalOutputs, } from './transaction/descriptor/verifyTransaction'; import { assertDescriptorWalletAddress, getDescriptorMapFromWallet, isDescriptorWallet } from './descriptor'; -import { - getFullNameFromCoinName, - getMainnetCoinName, - getNetworkFromCoinName, - isMainnetCoin, - UtxoCoinName, - UtxoCoinNameMainnet, -} from './names'; +import { getFullNameFromCoinName, getMainnetCoinName, isMainnetCoin, UtxoCoinName, UtxoCoinNameMainnet } from './names'; import { assertFixedScriptWalletAddress } from './address/fixedScript'; import { ParsedTransaction } from './transaction/types'; import { decodeDescriptorPsbt, decodePsbt, encodeTransaction, stringToBufferTryFormats } from './transaction/decode'; @@ -414,14 +407,6 @@ export abstract class AbstractUtxoCoin extends BaseCoin implements Musig2Partici this.amountType = amountType; } - /** - * @deprecated - will be removed when we drop support for utxolib - * Use `name` property instead. - */ - get network(): utxolib.Network { - return getNetworkFromCoinName(this.name); - } - getChain(): UtxoCoinName { return this.name; } diff --git a/modules/abstract-utxo/src/names.ts b/modules/abstract-utxo/src/names.ts index 555797ae3c..9e1a5c74c2 100644 --- a/modules/abstract-utxo/src/names.ts +++ b/modules/abstract-utxo/src/names.ts @@ -1,5 +1,3 @@ -import * as utxolib from '@bitgo/utxo-lib'; - export const utxoCoinsMainnet = ['btc', 'bch', 'bcha', 'bsv', 'btg', 'dash', 'doge', 'ltc', 'zec'] as const; export const utxoCoinsTestnet = [ 'tbtc', @@ -46,107 +44,6 @@ export function getMainnetCoinName(coinName: UtxoCoinName): UtxoCoinNameMainnet } } -function getNetworkName(n: utxolib.Network): utxolib.NetworkName { - const name = utxolib.getNetworkName(n); - if (!name) { - throw new Error('Unknown network'); - } - return name; -} - -/** - * @deprecated - will be removed when we drop support for utxolib - * @param n - * @returns the family name for a network. Testnets and mainnets of the same coin share the same family name. - */ -export function getFamilyFromNetwork(n: utxolib.Network): UtxoCoinNameMainnet { - switch (getNetworkName(n)) { - case 'bitcoin': - case 'testnet': - case 'bitcoinPublicSignet': - case 'bitcoinTestnet4': - case 'bitcoinBitGoSignet': - return 'btc'; - case 'bitcoincash': - case 'bitcoincashTestnet': - return 'bch'; - case 'ecash': - case 'ecashTest': - return 'bcha'; - case 'bitcoingold': - case 'bitcoingoldTestnet': - return 'btg'; - case 'bitcoinsv': - case 'bitcoinsvTestnet': - return 'bsv'; - case 'dash': - case 'dashTest': - return 'dash'; - case 'dogecoin': - case 'dogecoinTest': - return 'doge'; - case 'litecoin': - case 'litecoinTest': - return 'ltc'; - case 'zcash': - case 'zcashTest': - return 'zec'; - } -} - -/** - * @deprecated - will be removed when we drop support for utxolib - * Get the chain name for a network. - * The chain is different for every network. - */ -export function getCoinName(n: utxolib.Network): UtxoCoinName { - switch (getNetworkName(n)) { - case 'bitcoinPublicSignet': - return 'tbtcsig'; - case 'bitcoinTestnet4': - return 'tbtc4'; - case 'bitcoinBitGoSignet': - return 'tbtcbgsig'; - case 'bitcoin': - case 'testnet': - case 'bitcoincash': - case 'bitcoincashTestnet': - case 'ecash': - case 'ecashTest': - case 'bitcoingold': - case 'bitcoingoldTestnet': - case 'bitcoinsv': - case 'bitcoinsvTestnet': - case 'dash': - case 'dashTest': - case 'dogecoin': - case 'dogecoinTest': - case 'litecoin': - case 'litecoinTest': - case 'zcash': - case 'zcashTest': - const mainnetName = getFamilyFromNetwork(n); - return utxolib.isTestnet(n) ? `t${mainnetName}` : mainnetName; - } -} - -/** - * @deprecated - will be removed when we drop support for utxolib - * @param coinName - the name of the coin (e.g. 'btc', 'bch', 'ltc'). Also called 'chain' in some contexts. - * @returns the network for a coin. This is the mainnet network for the coin. - */ -export function getNetworkFromCoinName(coinName: string): utxolib.Network { - for (const network of utxolib.getNetworkList()) { - if (getCoinName(network) === coinName) { - return network; - } - } - throw new Error(`Unknown coin name ${coinName}`); -} - -/** @deprecated - use getNetworkFromCoinName instead */ -export const getNetworkFromChain = getNetworkFromCoinName; - function getBaseNameFromMainnet(coinName: UtxoCoinNameMainnet): string { switch (coinName) { case 'btc': @@ -189,11 +86,6 @@ export function getFullNameFromCoinName(coinName: UtxoCoinName): string { return prefix + getBaseNameFromMainnet(getMainnetCoinName(coinName)); } -/** @deprecated - use getFullNameFromCoinName instead */ -export function getFullNameFromNetwork(n: utxolib.Network): string { - return getFullNameFromCoinName(getCoinName(n)); -} - export function isTestnetCoin(coinName: UtxoCoinName): boolean { return isUtxoCoinNameTestnet(coinName); } diff --git a/modules/abstract-utxo/test/unit/buildSignSendLegacyFormat.ts b/modules/abstract-utxo/test/unit/buildSignSendLegacyFormat.ts index 2beb751815..a6b064904b 100644 --- a/modules/abstract-utxo/test/unit/buildSignSendLegacyFormat.ts +++ b/modules/abstract-utxo/test/unit/buildSignSendLegacyFormat.ts @@ -11,6 +11,7 @@ import { encryptKeychain, getDefaultWalletKeys, getMinUtxoCoins, + getNetworkForCoinName, getUtxoWallet, keychainsBase58, getScriptTypes, @@ -46,12 +47,17 @@ describe('prebuildAndSign-returnLegacyFormat', function () { const outputAmount = BigInt(inputScripts.length) * BigInt(1e8) - fee; const outputScriptType: utxolib.bitgo.outputScripts.ScriptType = 'p2sh'; const outputChain = utxolib.bitgo.getExternalChainCode(outputScriptType); - const outputAddress = utxolib.bitgo.getWalletAddress(rootWalletKeys, outputChain, 0, coin.network); + const outputAddress = utxolib.bitgo.getWalletAddress( + rootWalletKeys, + outputChain, + 0, + getNetworkForCoinName(coin.name) + ); recipient = { address: outputAddress, amount: outputAmount.toString() }; prebuild = utxolib.testutil.constructPsbt( inputScripts.map((s) => ({ scriptType: s, value: BigInt(1e8) })), [{ scriptType: outputScriptType, value: outputAmount }], - coin.network, + getNetworkForCoinName(coin.name), rootWalletKeys, 'unsigned' ); diff --git a/modules/abstract-utxo/test/unit/customSigner.ts b/modules/abstract-utxo/test/unit/customSigner.ts index 0e673009aa..681c4155ff 100644 --- a/modules/abstract-utxo/test/unit/customSigner.ts +++ b/modules/abstract-utxo/test/unit/customSigner.ts @@ -3,7 +3,14 @@ import nock = require('nock'); import * as sinon from 'sinon'; import { CustomSigningFunction, common } from '@bitgo/sdk-core'; -import { defaultBitGo, getDefaultWalletKeys, getUtxoCoin, getUtxoWallet, assertHasProperty } from './util'; +import { + defaultBitGo, + getDefaultWalletKeys, + getNetworkForCoinName, + getUtxoCoin, + getUtxoWallet, + assertHasProperty, +} from './util'; nock.disableNetConnect(); @@ -56,7 +63,7 @@ describe('UTXO Custom Signer Function', function () { const psbt = utxoLib.testutil.constructPsbt( [{ scriptType: 'taprootKeyPathSpend', value: BigInt(1000) }], [{ scriptType: 'p2sh', value: BigInt(900) }], - basecoin.network, + getNetworkForCoinName(basecoin.name), rootWalletKey, 'unsigned' ); @@ -72,7 +79,7 @@ describe('UTXO Custom Signer Function', function () { const psbt = utxoLib.testutil.constructPsbt( [{ scriptType: 'p2wsh', value: BigInt(1000) }], [{ scriptType: 'p2sh', value: BigInt(900) }], - basecoin.network, + getNetworkForCoinName(basecoin.name), rootWalletKey, 'unsigned' ); diff --git a/modules/abstract-utxo/test/unit/prebuildAndSign.ts b/modules/abstract-utxo/test/unit/prebuildAndSign.ts index e2ae0dd0ec..24910bea5b 100644 --- a/modules/abstract-utxo/test/unit/prebuildAndSign.ts +++ b/modules/abstract-utxo/test/unit/prebuildAndSign.ts @@ -12,6 +12,7 @@ import { defaultBitGo, getDefaultWalletKeys, getMinUtxoCoins, + getNetworkForCoinName, getUtxoWallet, keychainsBase58, getScriptTypes, @@ -62,7 +63,7 @@ function run(coin: AbstractUtxoCoin, inputScripts: ScriptType[]): void { const psbt = utxolib.testutil.constructPsbt( inputs as utxolib.testutil.Input[], outputs, - coin.network, + getNetworkForCoinName(coin.name), rootWalletKeys, 'unsigned', { @@ -178,7 +179,12 @@ function run(coin: AbstractUtxoCoin, inputScripts: ScriptType[]): void { const outputAmount = BigInt(inputScripts.length) * BigInt(1e8) - fee; const outputScriptType: utxolib.bitgo.outputScripts.ScriptType = 'p2sh'; const outputChain = utxolib.bitgo.getExternalChainCode(outputScriptType); - const outputAddress = utxolib.bitgo.getWalletAddress(rootWalletKeys, outputChain, 0, coin.network); + const outputAddress = utxolib.bitgo.getWalletAddress( + rootWalletKeys, + outputChain, + 0, + getNetworkForCoinName(coin.name) + ); recipient = { address: outputAddress, @@ -222,7 +228,7 @@ function run(coin: AbstractUtxoCoin, inputScripts: ScriptType[]): void { nocks.forEach((nock) => assert.ok(nock.isDone())); - assertSignable(res.txHex, inputScripts, coin.network); + assertSignable(res.txHex, inputScripts, getNetworkForCoinName(coin.name)); }); [true, false].forEach((selfSend) => { @@ -254,7 +260,7 @@ function run(coin: AbstractUtxoCoin, inputScripts: ScriptType[]): void { nocks.forEach((nock) => assert.ok(nock.isDone())); - assertSignable(res.txHex, inputScripts, coin.network); + assertSignable(res.txHex, inputScripts, getNetworkForCoinName(coin.name)); }); }); }); diff --git a/modules/abstract-utxo/test/unit/signTransaction.ts b/modules/abstract-utxo/test/unit/signTransaction.ts index 7491ecf82b..f5dfd0e111 100644 --- a/modules/abstract-utxo/test/unit/signTransaction.ts +++ b/modules/abstract-utxo/test/unit/signTransaction.ts @@ -9,7 +9,14 @@ import { common, Triple } from '@bitgo/sdk-core'; import { getReplayProtectionPubkeys, ErrorDeprecatedTxFormat } from '../../src'; import type { Unspent } from '../../src/unspent'; -import { getUtxoWallet, getDefaultWalletKeys, getUtxoCoin, keychainsBase58, defaultBitGo } from './util'; +import { + getUtxoWallet, + getDefaultWalletKeys, + getNetworkForCoinName, + getUtxoCoin, + keychainsBase58, + defaultBitGo, +} from './util'; describe('signTransaction', function () { const bgUrl = common.Environments[defaultBitGo.getEnv()].uri; @@ -21,7 +28,7 @@ describe('signTransaction', function () { const pubs = keychainsBase58.map((v) => v.pub) as Triple; function validatePsbt(txHex: string, targetSigCount: 0 | 1, targetNonceCount?: 1 | 2) { - const psbt = utxolib.bitgo.createPsbtFromHex(txHex, coin.network); + const psbt = utxolib.bitgo.createPsbtFromHex(txHex, getNetworkForCoinName(coin.name)); psbt.data.inputs.forEach((input, index) => { const parsed = utxolib.bitgo.parsePsbtInput(input); if (parsed.scriptType === 'taprootKeyPathSpend') { @@ -38,7 +45,7 @@ describe('signTransaction', function () { } function validateTx(txHex: string, unspents: Unspent[], targetSigCount: 0 | 1) { - const tx = utxolib.bitgo.createTransactionFromHex(txHex, coin.network); + const tx = utxolib.bitgo.createTransactionFromHex(txHex, getNetworkForCoinName(coin.name)); unspents.forEach((u, i) => { const sigCount = utxolib.bitgo.getStrictSignatureCount(tx.ins[i]); const expectedSigCount = utxolib.bitgo.isWalletUnspent(u) && !!targetSigCount ? 1 : 0; @@ -56,7 +63,7 @@ describe('signTransaction', function () { const txHex = tx.toHex(); function nockSignPsbt(psbtHex: string): nock.Scope { - const psbt = utxolib.bitgo.createPsbtFromHex(psbtHex, coin.network); + const psbt = utxolib.bitgo.createPsbtFromHex(psbtHex, getNetworkForCoinName(coin.name)); return nock(bgUrl) .post(`/api/v2/${wallet.coin()}/wallet/${wallet.id()}/tx/signpsbt`, (body) => body.psbt) .reply(200, { psbt: psbt.setAllInputsMusig2NonceHD(rootWalletKeys.bitgo).toHex() }); @@ -147,7 +154,7 @@ describe('signTransaction', function () { .map((scriptType) => ({ scriptType, value: BigInt(1000) })); const unspentSum = inputs.reduce((prev: bigint, curr) => prev + curr.value, BigInt(0)); const outputs: testutil.Output[] = [{ scriptType: 'p2sh', value: unspentSum - BigInt(1000) }]; - const psbt = testutil.constructPsbt(inputs, outputs, coin.network, rootWalletKeys, 'unsigned', { + const psbt = testutil.constructPsbt(inputs, outputs, getNetworkForCoinName(coin.name), rootWalletKeys, 'unsigned', { p2shP2pkKey: replayProtectionKey, }); @@ -163,7 +170,7 @@ describe('signTransaction', function () { .map((scriptType) => ({ scriptType, value: BigInt(1000) })); const unspentSum = inputs.reduce((prev: bigint, cur) => prev + cur.value, BigInt(0)); const outputs: testutil.Output[] = [{ scriptType: 'p2sh', value: unspentSum - BigInt(1000) }]; - const psbt = testutil.constructPsbt(inputs, outputs, coin.network, rootWalletKeys, 'unsigned', { + const psbt = testutil.constructPsbt(inputs, outputs, getNetworkForCoinName(coin.name), rootWalletKeys, 'unsigned', { p2shP2pkKey: replayProtectionKey, }); @@ -181,8 +188,16 @@ describe('signTransaction', function () { })); const unspentSum = inputs.reduce((prev: bigint, curr) => prev + curr.value, BigInt(0)); const outputs: testutil.TxnOutput[] = [{ scriptType: 'p2sh', value: unspentSum - BigInt(1000) }]; - const txBuilder = testutil.constructTxnBuilder(inputs, outputs, coin.network, rootWalletKeys, 'unsigned'); - const unspents = inputs.map((v, i) => testutil.toTxnUnspent(v, i, coin.network, rootWalletKeys)); + const txBuilder = testutil.constructTxnBuilder( + inputs, + outputs, + getNetworkForCoinName(coin.name), + rootWalletKeys, + 'unsigned' + ); + const unspents = inputs.map((v, i) => + testutil.toTxnUnspent(v, i, getNetworkForCoinName(coin.name), rootWalletKeys) + ); // Legacy format transactions are now deprecated and should throw ErrorDeprecatedTxFormat await assert.rejects(async () => { @@ -194,7 +209,7 @@ describe('signTransaction', function () { const inputs: testutil.Input[] = [{ scriptType: 'taprootKeyPathSpend', value: BigInt(1000) }]; const unspentSum = inputs.reduce((prev: bigint, curr) => prev + curr.value, BigInt(0)); const outputs: testutil.Output[] = [{ scriptType: 'p2sh', value: unspentSum - BigInt(1000) }]; - const psbt = testutil.constructPsbt(inputs, outputs, coin.network, rootWalletKeys, 'unsigned'); + const psbt = testutil.constructPsbt(inputs, outputs, getNetworkForCoinName(coin.name), rootWalletKeys, 'unsigned'); await assert.rejects( async () => { diff --git a/modules/abstract-utxo/test/unit/transaction.ts b/modules/abstract-utxo/test/unit/transaction.ts index ed69fa610e..09f8d51201 100644 --- a/modules/abstract-utxo/test/unit/transaction.ts +++ b/modules/abstract-utxo/test/unit/transaction.ts @@ -31,6 +31,7 @@ import { getWalletKeys, defaultBitGo, getMinUtxoCoins, + getNetworkForCoinName, getScriptTypes, } from './util'; @@ -60,7 +61,7 @@ function run( return testutil.toUnspent( { scriptType: t, value: t === 'p2shP2pk' ? BigInt(1000) : BigInt(value) }, index, - coin.network, + getNetworkForCoinName(coin.name), walletKeys ); }); @@ -72,7 +73,7 @@ function run( function getUnspents(): Unspent[] { return inputScripts.map((type, i) => - mockUnspent(coin.network, walletKeys, toTxnInputScriptType(type), i, value) + mockUnspent(getNetworkForCoinName(coin.name), walletKeys, toTxnInputScriptType(type), i, value) ); } @@ -113,7 +114,9 @@ function run( scope = nock(bgUrl) .post(`/api/v2/${wallet.coin()}/wallet/${wallet.id()}/tx/signpsbt`, (body) => body.psbt) .reply(200, (_uri: string, requestBody: unknown) => { - const networkName = utxolib.getNetworkName(coin.network) as fixedScriptWallet.NetworkName; + const networkName = utxolib.getNetworkName( + getNetworkForCoinName(coin.name) + ) as fixedScriptWallet.NetworkName; const reqBytes = Buffer.from((requestBody as { psbt: string }).psbt, 'hex'); const reqPsbt = fixedScriptWallet.BitGoPsbt.fromBytes(reqBytes, networkName); const cosignerWasm = BIP32.fromBase58(cosigner.toBase58()); @@ -166,7 +169,7 @@ function run( const outputs: testutil.Output[] = [ { address: getOutputAddress(getWalletKeys('test')), value: unspentSum - BigInt(1000) }, ]; - const psbt = testutil.constructPsbt(inputs, outputs, coin.network, walletKeys, 'unsigned', { + const psbt = testutil.constructPsbt(inputs, outputs, getNetworkForCoinName(coin.name), walletKeys, 'unsigned', { p2shP2pkKey: getReplayProtectionPubkeys(coin.name)[0], }); utxolib.bitgo.addXpubsToPsbt(psbt, walletKeys); @@ -177,7 +180,11 @@ function run( const prebuild = txFormat === 'psbt' ? createPrebuildPsbt() - : createPrebuildTransaction(coin.network, getUnspents(), getOutputAddress(walletKeys)); + : createPrebuildTransaction( + getNetworkForCoinName(coin.name), + getUnspents(), + getOutputAddress(walletKeys) + ); const halfSignedUserBitGo = await createHalfSignedTransaction(prebuild, walletKeys.user, walletKeys.bitgo); const fullSignedUserBitGo = @@ -225,7 +232,7 @@ function run( ? undefined : v instanceof utxolib.bitgo.UtxoTransaction ? transactionToObj(v) - : transactionHexToObj(v.txHex, coin.network, amountType) + : transactionHexToObj(v.txHex, getNetworkForCoinName(coin.name), amountType) ) as TransactionObjStages; } @@ -240,7 +247,7 @@ function run( }); function testPsbtValidSignatures(tx: HalfSignedUtxoTransaction, signedBy: BIP32Interface[]) { - const psbt = utxolib.bitgo.createPsbtFromHex(tx.txHex, coin.network); + const psbt = utxolib.bitgo.createPsbtFromHex(tx.txHex, getNetworkForCoinName(coin.name)); const unspents = getUnspentsForPsbt(); psbt.data.inputs.forEach((input, index) => { const unspent = unspents[index]; @@ -278,7 +285,7 @@ function run( const transaction = utxolib.bitgo.createTransactionFromBuffer( Buffer.from(tx.txHex, 'hex'), - coin.network, + getNetworkForCoinName(coin.name), { amountType } ); transaction.ins.forEach((input, index) => { diff --git a/modules/abstract-utxo/test/unit/transaction/fixedScript/parsePsbt.ts b/modules/abstract-utxo/test/unit/transaction/fixedScript/parsePsbt.ts index d129f40614..e6c2676073 100644 --- a/modules/abstract-utxo/test/unit/transaction/fixedScript/parsePsbt.ts +++ b/modules/abstract-utxo/test/unit/transaction/fixedScript/parsePsbt.ts @@ -8,10 +8,9 @@ import { fixedScriptWallet } from '@bitgo/wasm-utxo'; import { parseTransaction } from '../../../../src/transaction/fixedScript/parseTransaction'; import { ParsedTransaction } from '../../../../src/transaction/types'; import { UtxoWallet } from '../../../../src/wallet'; -import { getUtxoCoin } from '../../util'; +import { getCoinNameForNetwork, getUtxoCoin } from '../../util'; import { explainPsbtWasm } from '../../../../src/transaction/fixedScript'; import type { TransactionExplanation } from '../../../../src/transaction/fixedScript/explainTransaction'; -import { getCoinName } from '../../../../src/names'; import { TransactionPrebuild } from '../../../../src/abstractUtxoCoin'; function getTxParamsFromExplanation( @@ -78,7 +77,7 @@ function describeParseTransactionWith( let stubExplainTransaction: sinon.SinonStub; before('prepare', async function () { - const coinName = getCoinName(acidTest.network); + const coinName = getCoinNameForNetwork(acidTest.network); coin = getUtxoCoin(coinName); // Create PSBT and explanation diff --git a/modules/abstract-utxo/test/unit/wallet.ts b/modules/abstract-utxo/test/unit/wallet.ts index 7c9b6223e7..4313a70237 100644 --- a/modules/abstract-utxo/test/unit/wallet.ts +++ b/modules/abstract-utxo/test/unit/wallet.ts @@ -5,7 +5,7 @@ import nock = require('nock'); import * as _ from 'lodash'; import { Wallet, ManageUnspentsOptions, common } from '@bitgo/sdk-core'; -import { defaultBitGo, getDefaultWalletKeys, toKeychainObjects, getUtxoCoin } from './util'; +import { defaultBitGo, getDefaultWalletKeys, getNetworkForCoinName, toKeychainObjects, getUtxoCoin } from './util'; const bgUrl = common.Environments[defaultBitGo.getEnv()].uri; const bitgo = defaultBitGo; @@ -35,7 +35,7 @@ describe('manage unspents', function () { utxoLib.testutil.constructPsbt( [{ scriptType, value: BigInt(1000) }], [{ scriptType, value: BigInt(900) }], - basecoin.network, + getNetworkForCoinName(basecoin.name), rootWalletKey, 'unsigned' ) @@ -74,7 +74,7 @@ describe('manage unspents', function () { const psbt = utxoLib.testutil.constructPsbt( [{ scriptType: 'p2wsh', value: BigInt(1000) }], [{ scriptType: 'p2shP2wsh', value: BigInt(900) }], - basecoin.network, + getNetworkForCoinName(basecoin.name), rootWalletKey, 'unsigned' ); From ea0b1379cf362786d57f7ee0e878393dcbfb7a47 Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Fri, 22 May 2026 15:23:48 +0200 Subject: [PATCH 21/34] refactor(abstract-utxo): inline ScriptType2Of3 type definition Replace the utxolib.bitgo.outputScripts.ScriptType2Of3 type re-export with an inline literal union. The values are identical to the utxolib export ('p2sh' | 'p2shP2wsh' | 'p2wsh' | 'p2tr' | 'p2trMusig2'). This removes the last utxolib type reference and the top-level utxolib import from abstractUtxoCoin.ts. Refs: T1-3279 --- modules/abstract-utxo/src/abstractUtxoCoin.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/modules/abstract-utxo/src/abstractUtxoCoin.ts b/modules/abstract-utxo/src/abstractUtxoCoin.ts index 530fb9ffa2..4ebe72d4c3 100644 --- a/modules/abstract-utxo/src/abstractUtxoCoin.ts +++ b/modules/abstract-utxo/src/abstractUtxoCoin.ts @@ -2,7 +2,6 @@ import assert from 'assert'; import { randomBytes } from 'crypto'; import _ from 'lodash'; -import * as utxolib from '@bitgo/utxo-lib'; import { address as wasmAddress, BIP32, fixedScriptWallet, hasPsbtMagic } from '@bitgo/wasm-utxo'; import { AddressCoinSpecific, @@ -87,7 +86,7 @@ import { isUtxoWalletData, UtxoWallet } from './wallet'; import { isDescriptorWalletData } from './descriptor/descriptorWallet'; import type { Unspent } from './unspent'; -import ScriptType2Of3 = utxolib.bitgo.outputScripts.ScriptType2Of3; +type ScriptType2Of3 = 'p2sh' | 'p2shP2wsh' | 'p2wsh' | 'p2tr' | 'p2trMusig2'; export type TxFormat = // This is a legacy transaction format based around the bitcoinjs-lib serialization of unsigned transactions @@ -133,8 +132,6 @@ type UtxoCustomSigningFunction = { }): Promise; }; -const { ChainCode } = fixedScriptWallet; - /** * Check if a decoded transaction has at least one taproot key path spend (MuSig2) input. */ From 0547f9859cb413c6ff4bb4e37fd4d7309723e8d2 Mon Sep 17 00:00:00 2001 From: rajangarg047 Date: Tue, 9 Jun 2026 14:07:30 -0400 Subject: [PATCH 22/34] feat(sdk-core): send webauthnInfo with enterpriseId for MPC user keychain MPC/TSS wallet creation attached the user keychain's passkey by sending a bare webauthnDevices array (no enterpriseId) on POST /api/v2/:coin/key. The wallet-platform atomic key-creation endpoint only consumes webauthnInfo (a single object including enterpriseId, used to validate the PRF salt) and ignores webauthnDevices on input, so passkeys were never persisted for TSS/MPC user keychains. Switch MPC user-keychain creation to send webauthnInfo with enterpriseId, mirroring the onchain key-creation contract. Applied across all four MPC keychain implementations (ECDSA + EdDSA, MPCv1 + MPCv2), threading the existing createKeychains enterprise param down to the USER participant, and widen WebauthnInfo with optional enterpriseId. Add unit tests asserting webauthnInfo (with enterpriseId) is sent on the user keychain across all four MPC paths, that the deprecated webauthnDevices array is not sent, and that the PRF-encrypted prv decrypts with the webauthn passphrase. WCN-848 Co-Authored-By: Claude Opus 4.8 (1M context) --- .../test/v2/unit/internal/tssUtils/ecdsa.ts | 43 ++++++++++++ .../tssUtils/ecdsaMPCv2/createKeychains.ts | 65 +++++++++++++++++++ .../test/v2/unit/internal/tssUtils/eddsa.ts | 58 +++++++++++++++++ .../tssUtils/eddsaMPCv2/createKeychains.ts | 46 +++++++++++++ .../sdk-core/src/bitgo/keychain/iKeychains.ts | 2 + .../src/bitgo/utils/tss/ecdsa/ecdsa.ts | 39 ++++++----- .../src/bitgo/utils/tss/ecdsa/ecdsaMPCv2.ts | 35 +++++----- .../src/bitgo/utils/tss/eddsa/eddsa.ts | 25 +++---- .../src/bitgo/utils/tss/eddsa/eddsaMPCv2.ts | 35 +++++----- 9 files changed, 290 insertions(+), 58 deletions(-) diff --git a/modules/bitgo/test/v2/unit/internal/tssUtils/ecdsa.ts b/modules/bitgo/test/v2/unit/internal/tssUtils/ecdsa.ts index 6e9c010a43..cb3cc68216 100644 --- a/modules/bitgo/test/v2/unit/internal/tssUtils/ecdsa.ts +++ b/modules/bitgo/test/v2/unit/internal/tssUtils/ecdsa.ts @@ -14,6 +14,7 @@ import { import { BitGo, createSharedDataProof, TssUtils, RequestType } from '../../../../../src'; import { BackupGpgKey, + AddKeychainOptions, BackupKeyShare, BaseCoin, BitgoGPGPublicKey, @@ -309,6 +310,48 @@ describe('TSS Ecdsa Utils:', async function () { should.exist(backupKeychain.encryptedPrv); }); + it('should send webauthnInfo (with enterpriseId) on the user keychain when webauthnInfo is provided', async function () { + // Keep the real crypto deps (constants w/ bitgo gpg key for verifyWalletSignatures) and + // capture the user keychain add() params by stubbing baseCoin.keychains(). + nock.cleanAll(); + nock(bgUrl) + .get('/api/v1/client/constants') + .times(16) + .reply(200, { ttl: 3600, constants: { mpc: { bitgoPublicKey: bitGoGPGKeyPair.publicKey } } }); + + const addStub = sandbox.stub().resolves({ id: '1', pub: '', type: 'tss' }); + sandbox.stub(baseCoin, 'keychains').returns({ add: addStub } as unknown as ReturnType); + + const enterpriseId = 'enterprise_id'; + const webauthnInfo = { otpDeviceId: 'device-123', prfSalt: 'salt-abc', passphrase: 'prf-derived-passphrase' }; + await tssUtils.createParticipantKeychain( + userGpgKey, + userLocalBackupGpgKey, + bitgoPublicKey, + 1, + userKeyShare, + backupKeyShare, + nockedBitGoKeychain, + 'passphrase', + undefined, + webauthnInfo, + undefined, + enterpriseId + ); + + // User keychain must carry webauthnInfo (the field the backend POST /key consumes), including + // enterpriseId, and must NOT use the deprecated webauthnDevices array. + assert.ok(addStub.calledOnce, 'keychains().add should have been called for the user keychain'); + const body = addStub.firstCall.args[0] as AddKeychainOptions; + assert.ok(body.webauthnInfo, 'user keychain body should include webauthnInfo'); + assert.equal(body.webauthnInfo.otpDeviceId, webauthnInfo.otpDeviceId); + assert.equal(body.webauthnInfo.prfSalt, webauthnInfo.prfSalt); + assert.equal(body.webauthnInfo.enterpriseId, enterpriseId); + assert.ok(body.webauthnInfo.encryptedPrv, 'encryptedPrv should be set'); + assert.ok(bitgo.decrypt({ input: body.webauthnInfo.encryptedPrv, password: webauthnInfo.passphrase })); + assert.strictEqual(body.webauthnDevices, undefined, 'deprecated webauthnDevices should not be sent'); + }); + it('should generate TSS key chains with optional params', async function () { const enterprise = 'enterprise_id'; const backupShareHolder: BackupKeyShare = { diff --git a/modules/bitgo/test/v2/unit/internal/tssUtils/ecdsaMPCv2/createKeychains.ts b/modules/bitgo/test/v2/unit/internal/tssUtils/ecdsaMPCv2/createKeychains.ts index dee40f849f..94e6d08830 100644 --- a/modules/bitgo/test/v2/unit/internal/tssUtils/ecdsaMPCv2/createKeychains.ts +++ b/modules/bitgo/test/v2/unit/internal/tssUtils/ecdsaMPCv2/createKeychains.ts @@ -57,6 +57,9 @@ describe('TSS Ecdsa MPCv2 Utils:', async function () { }); before(async function () { + // Allow secp256k1 GPG keys used by these fixtures (the full suite enables this + // globally via sibling test files; set it here so this file also runs in isolation). + openpgp.config.rejectCurves = new Set(); bitGoGgpKey = await openpgp.generateKey({ userIDs: [ { @@ -176,6 +179,68 @@ describe('TSS Ecdsa MPCv2 Utils:', async function () { assert.equal(bitgoKeychain.source, 'bitgo'); }); + it('should send webauthnInfo (with enterpriseId) on the user keychain when webauthnInfo is provided', async function () { + const bitgoSession = new DklsDkg.Dkg(3, 2, 2); + + const round1Nock = await nockKeyGenRound1(bitgoSession, 1); + const round2Nock = await nockKeyGenRound2(bitgoSession, 1); + const round3Nock = await nockKeyGenRound3(bitgoSession, 1); + + // Capture each keychain POST body by source so we can assert what the user key sends. + const capturedBodies: Record = {}; + const addKeyNock = nock('https://bitgo.fakeurl') + .post(`/api/v2/${coinName}/key`, (body) => body.keyType === 'tss' && body.isMPCv2) + .times(3) + .reply(200, async (uri, requestBody: AddKeychainOptions) => { + capturedBodies[requestBody.source as string] = requestBody; + const key = { + id: requestBody.source, + source: requestBody.source, + type: requestBody.keyType, + commonKeychain: requestBody.commonKeychain, + encryptedPrv: requestBody.encryptedPrv, + }; + nock('https://bitgo.fakeurl').get(`/api/v2/${coinName}/key/${requestBody.source}`).reply(200, key); + return key; + }); + + const webauthnInfo = { + otpDeviceId: 'device-123', + prfSalt: 'salt-abc', + passphrase: 'prf-derived-passphrase', + }; + const params = { + passphrase: 'test', + enterprise: enterpriseId, + originalPasscodeEncryptionCode: '123456', + webauthnInfo, + }; + await tssUtils.createKeychains(params); + assert.ok(round1Nock.isDone()); + assert.ok(round2Nock.isDone()); + assert.ok(round3Nock.isDone()); + assert.ok(addKeyNock.isDone()); + + // User keychain must carry webauthnInfo (the field the backend POST /key consumes), + // including enterpriseId, and must NOT use the deprecated webauthnDevices array. + const userBody = capturedBodies['user']; + assert.ok(userBody, 'user keychain should have been created'); + assert.ok(userBody.webauthnInfo, 'user keychain body should include webauthnInfo'); + assert.equal(userBody.webauthnInfo.otpDeviceId, webauthnInfo.otpDeviceId); + assert.equal(userBody.webauthnInfo.prfSalt, webauthnInfo.prfSalt); + assert.equal(userBody.webauthnInfo.enterpriseId, enterpriseId); + assert.ok(userBody.webauthnInfo.encryptedPrv, 'encryptedPrv should be set'); + // encryptedPrv is the user key share encrypted with the PRF-derived passphrase. + assert.ok(bitgo.decrypt({ input: userBody.webauthnInfo.encryptedPrv, password: webauthnInfo.passphrase })); + assert.strictEqual(userBody.webauthnDevices, undefined, 'deprecated webauthnDevices should not be sent'); + + // Backup keychain must never carry passkey material. + const backupBody = capturedBodies['backup']; + assert.ok(backupBody, 'backup keychain should have been created'); + assert.strictEqual(backupBody.webauthnInfo, undefined); + assert.strictEqual(backupBody.webauthnDevices, undefined); + }); + it('should generate TSS MPCv2 keys with v2 encryption envelopes', async function () { const bitgoSession = new DklsDkg.Dkg(3, 2, 2); diff --git a/modules/bitgo/test/v2/unit/internal/tssUtils/eddsa.ts b/modules/bitgo/test/v2/unit/internal/tssUtils/eddsa.ts index d3dea3383b..f1daae1f91 100644 --- a/modules/bitgo/test/v2/unit/internal/tssUtils/eddsa.ts +++ b/modules/bitgo/test/v2/unit/internal/tssUtils/eddsa.ts @@ -1,3 +1,4 @@ +import * as assert from 'assert'; import * as sodium from 'libsodium-wrappers-sumo'; import * as _ from 'lodash'; import nock = require('nock'); @@ -8,6 +9,7 @@ import * as sinon from 'sinon'; import { TestableBG, TestBitGo } from '@bitgo/sdk-test'; import { BitGo } from '../../../../../src'; import { + AddKeychainOptions, BaseCoin, BitgoGPGPublicKey, CommitmentShareRecord, @@ -269,6 +271,62 @@ describe('TSS Utils:', async function () { should.exist(backupKeychain.encryptedPrv); }); + it('should send webauthnInfo (with enterpriseId) on the user keychain when webauthnInfo is provided', async function () { + const userKeyShare = MPC.keyShare(1, 2, 3); + const backupKeyShare = MPC.keyShare(2, 2, 3); + + // Real crypto deps (constants w/ bitgo gpg key for verifyWalletSignatures + bitgo keychain), + // then capture the user keychain add() params by stubbing baseCoin.keychains(). + nock.cleanAll(); + nock(bgUrl) + .get('/api/v1/client/constants') + .times(23) + .reply(200, { ttl: 3600, constants: { mpc: { bitgoPublicKey: bitgoGpgKey.publicKey } } }); + await nockBitgoKeychain({ + coin: coinName, + userKeyShare, + backupKeyShare, + bitgoKeyShare, + userGpgKey, + backupGpgKey, + bitgoGpgKey, + }); + const bitgoKeychain = await tssUtils.createBitgoKeychain({ + userGpgKey, + backupGpgKey, + userKeyShare, + backupKeyShare, + }); + + const addStub = sandbox.stub().resolves({ id: '1', pub: '', type: 'tss' }); + sandbox.stub(baseCoin, 'keychains').returns({ add: addStub } as unknown as ReturnType); + + const enterpriseId = 'enterprise_id'; + const webauthnInfo = { otpDeviceId: 'device-123', prfSalt: 'salt-abc', passphrase: 'prf-derived-passphrase' }; + await tssUtils.createUserKeychain({ + userGpgKey, + backupGpgKey, + userKeyShare, + backupKeyShare, + bitgoKeychain, + passphrase: 'passphrase', + webauthnInfo, + enterprise: enterpriseId, + }); + + // User keychain must carry webauthnInfo (the field the backend POST /key consumes), including + // enterpriseId, and must NOT use the deprecated webauthnDevices array. + assert.ok(addStub.calledOnce, 'keychains().add should have been called for the user keychain'); + const body = addStub.firstCall.args[0] as AddKeychainOptions; + assert.ok(body.webauthnInfo, 'user keychain body should include webauthnInfo'); + assert.equal(body.webauthnInfo.otpDeviceId, webauthnInfo.otpDeviceId); + assert.equal(body.webauthnInfo.prfSalt, webauthnInfo.prfSalt); + assert.equal(body.webauthnInfo.enterpriseId, enterpriseId); + assert.ok(body.webauthnInfo.encryptedPrv, 'encryptedPrv should be set'); + assert.ok(bitgo.decrypt({ input: body.webauthnInfo.encryptedPrv, password: webauthnInfo.passphrase })); + assert.strictEqual(body.webauthnDevices, undefined, 'deprecated webauthnDevices should not be sent'); + }); + it('should generate TSS key chains without passphrase', async function () { const userKeyShare = MPC.keyShare(1, 2, 3); const backupKeyShare = MPC.keyShare(2, 2, 3); diff --git a/modules/bitgo/test/v2/unit/internal/tssUtils/eddsaMPCv2/createKeychains.ts b/modules/bitgo/test/v2/unit/internal/tssUtils/eddsaMPCv2/createKeychains.ts index 6cb2f33216..53ae6fe795 100644 --- a/modules/bitgo/test/v2/unit/internal/tssUtils/eddsaMPCv2/createKeychains.ts +++ b/modules/bitgo/test/v2/unit/internal/tssUtils/eddsaMPCv2/createKeychains.ts @@ -109,6 +109,52 @@ describe('TSS EdDSA MPCv2 Utils:', async function () { assert.equal(userKeychain.commonKeychain, bitgoKeychain.commonKeychain); }); + it('should send webauthnInfo (with enterpriseId) on the user keychain when webauthnInfo is provided', async function () { + const commonKeychain = 'a'.repeat(64); + const capturedBodies: Record = {}; + const addKeyNock = nock('https://bitgo.fakeurl') + .post(`/api/v2/${coinName}/key`, (body) => body.keyType === 'tss' && body.isMPCv2) + .times(1) + .reply(200, async (uri, requestBody: AddKeychainOptions) => { + capturedBodies[requestBody.source as string] = requestBody; + return { + id: requestBody.source, + source: requestBody.source, + type: requestBody.keyType, + commonKeychain: requestBody.commonKeychain, + encryptedPrv: requestBody.encryptedPrv, + }; + }); + + const webauthnInfo = { otpDeviceId: 'device-123', prfSalt: 'salt-abc', passphrase: 'prf-derived-passphrase' }; + // Direct participant-keychain call avoids the EdDSA DKG ceremony while still exercising the + // user-keychain webauthn assembly that POSTs to /key. + await tssUtils.createParticipantKeychain( + MPCv2PartiesEnum.USER, + commonKeychain, + Buffer.from('userPrivate'), + Buffer.from('userReduced'), + 'passphrase', + undefined, + webauthnInfo, + undefined, + enterpriseId + ); + assert.ok(addKeyNock.isDone()); + + // User keychain must carry webauthnInfo (the field the backend POST /key consumes), including + // enterpriseId, and must NOT use the deprecated webauthnDevices array. + const userBody = capturedBodies['user']; + assert.ok(userBody, 'user keychain should have been created'); + assert.ok(userBody.webauthnInfo, 'user keychain body should include webauthnInfo'); + assert.equal(userBody.webauthnInfo.otpDeviceId, webauthnInfo.otpDeviceId); + assert.equal(userBody.webauthnInfo.prfSalt, webauthnInfo.prfSalt); + assert.equal(userBody.webauthnInfo.enterpriseId, enterpriseId); + assert.ok(userBody.webauthnInfo.encryptedPrv, 'encryptedPrv should be set'); + assert.ok(bitgo.decrypt({ input: userBody.webauthnInfo.encryptedPrv, password: webauthnInfo.passphrase })); + assert.strictEqual(userBody.webauthnDevices, undefined, 'deprecated webauthnDevices should not be sent'); + }); + it('should create TSS key chains', async function () { const fakeCommonKeychain = 'a'.repeat(64); diff --git a/modules/sdk-core/src/bitgo/keychain/iKeychains.ts b/modules/sdk-core/src/bitgo/keychain/iKeychains.ts index e6c7d3dcb9..9feb9e6686 100644 --- a/modules/sdk-core/src/bitgo/keychain/iKeychains.ts +++ b/modules/sdk-core/src/bitgo/keychain/iKeychains.ts @@ -10,6 +10,8 @@ export interface WebauthnInfo { prfSalt: string; otpDeviceId: string; encryptedPrv: string; + /** Required by POST /key to validate the PRF salt; not needed on the PUT /key/:id update path. */ + enterpriseId?: string; } import type { WebauthnKeyEncryptionInfo } from '../wallet/iWallets'; diff --git a/modules/sdk-core/src/bitgo/utils/tss/ecdsa/ecdsa.ts b/modules/sdk-core/src/bitgo/utils/tss/ecdsa/ecdsa.ts index 3a3f7a5099..b386a23871 100644 --- a/modules/sdk-core/src/bitgo/utils/tss/ecdsa/ecdsa.ts +++ b/modules/sdk-core/src/bitgo/utils/tss/ecdsa/ecdsa.ts @@ -145,6 +145,7 @@ export class EcdsaUtils extends BaseEcdsaUtils { originalPasscodeEncryptionCode: params.originalPasscodeEncryptionCode, webauthnInfo: params.webauthnInfo, encryptionVersion: params.encryptionVersion, + enterprise: params.enterprise, }); const backupKeychainPromise = this.createBackupKeychain({ userGpgKey, @@ -187,6 +188,7 @@ export class EcdsaUtils extends BaseEcdsaUtils { originalPasscodeEncryptionCode, webauthnInfo, encryptionVersion, + enterprise, }: CreateEcdsaKeychainParams): Promise { if (!passphrase) { throw new Error('Please provide a wallet passphrase'); @@ -203,7 +205,8 @@ export class EcdsaUtils extends BaseEcdsaUtils { passphrase, originalPasscodeEncryptionCode, webauthnInfo, - encryptionVersion + encryptionVersion, + enterprise ); } @@ -322,7 +325,8 @@ export class EcdsaUtils extends BaseEcdsaUtils { passphrase: string, originalPasscodeEncryptionCode?: string, webauthnInfo?: WebauthnKeyEncryptionInfo, - encryptionVersion?: EncryptionVersion + encryptionVersion?: EncryptionVersion, + enterprise?: string ): Promise { const bitgoKeyShares = bitgoKeychain.keyShares; if (!bitgoKeyShares) { @@ -407,7 +411,7 @@ export class EcdsaUtils extends BaseEcdsaUtils { ); const prv = JSON.stringify(recipientCombinedKey.signingMaterial); - const recipientKeychainParams = { + const recipientKeychainParams: AddKeychainOptions & { prv: string } = { source: recipient, keyType: 'tss' as KeyType, commonKeychain: bitgoKeychain.commonKeychain, @@ -418,22 +422,23 @@ export class EcdsaUtils extends BaseEcdsaUtils { encryptionVersion, }), originalPasscodeEncryptionCode, - webauthnDevices: - webauthnInfo && recipientIndex === ShareKeyPosition.USER - ? [ - { - otpDeviceId: webauthnInfo.otpDeviceId, - prfSalt: webauthnInfo.prfSalt, - encryptedPrv: await this.bitgo.encryptAsync({ - input: prv, - password: webauthnInfo.passphrase, - encryptionVersion, - }), - }, - ] - : undefined, }; + if (webauthnInfo && recipientIndex === ShareKeyPosition.USER) { + // Send the passkey as `webauthnInfo`; the deprecated `webauthnDevices` array is ignored by POST /key. + assert(enterprise, 'enterprise is required to attach a webauthn device to the user keychain'); + recipientKeychainParams.webauthnInfo = { + otpDeviceId: webauthnInfo.otpDeviceId, + prfSalt: webauthnInfo.prfSalt, + encryptedPrv: await this.bitgo.encryptAsync({ + input: prv, + password: webauthnInfo.passphrase, + encryptionVersion, + }), + enterpriseId: enterprise, + }; + } + const keychains = this.baseCoin.keychains(); return recipientIndex === 1 ? await keychains.add(recipientKeychainParams) diff --git a/modules/sdk-core/src/bitgo/utils/tss/ecdsa/ecdsaMPCv2.ts b/modules/sdk-core/src/bitgo/utils/tss/ecdsa/ecdsaMPCv2.ts index 337d853157..03d16dd215 100644 --- a/modules/sdk-core/src/bitgo/utils/tss/ecdsa/ecdsaMPCv2.ts +++ b/modules/sdk-core/src/bitgo/utils/tss/ecdsa/ecdsaMPCv2.ts @@ -336,7 +336,8 @@ export class EcdsaMPCv2Utils extends BaseEcdsaUtils { params.originalPasscodeEncryptionCode, params.webauthnInfo, encryptionSession, - params.encryptionVersion + params.encryptionVersion, + params.enterprise ); const backupKeychainPromise = this.addBackupKeychain( bitgoCommonKeychain, @@ -380,7 +381,8 @@ export class EcdsaMPCv2Utils extends BaseEcdsaUtils { decrypt(ciphertext: string): Promise; destroy(): void; }, - encryptionVersion?: EncryptionVersion + encryptionVersion?: EncryptionVersion, + enterprise?: string ): Promise { let source: string; let encryptedPrv: string | undefined = undefined; @@ -435,17 +437,18 @@ export class EcdsaMPCv2Utils extends BaseEcdsaUtils { }; if (webauthnInfo && participantIndex === MPCv2PartiesEnum.USER && privateMaterialBase64) { - recipientKeychainParams.webauthnDevices = [ - { - otpDeviceId: webauthnInfo.otpDeviceId, - prfSalt: webauthnInfo.prfSalt, - encryptedPrv: await this.bitgo.encryptAsync({ - input: privateMaterialBase64, - password: webauthnInfo.passphrase, - encryptionVersion, - }), - }, - ]; + // Send the passkey as `webauthnInfo`; the deprecated `webauthnDevices` array is ignored by POST /key. + assert(enterprise, 'enterprise is required to attach a webauthn device to the user keychain'); + recipientKeychainParams.webauthnInfo = { + otpDeviceId: webauthnInfo.otpDeviceId, + prfSalt: webauthnInfo.prfSalt, + encryptedPrv: await this.bitgo.encryptAsync({ + input: privateMaterialBase64, + password: webauthnInfo.passphrase, + encryptionVersion, + }), + enterpriseId: enterprise, + }; } const keychains = this.baseCoin.keychains(); @@ -574,7 +577,8 @@ export class EcdsaMPCv2Utils extends BaseEcdsaUtils { decrypt(ciphertext: string): Promise; destroy(): void; }, - encryptionVersion?: EncryptionVersion + encryptionVersion?: EncryptionVersion, + enterprise?: string ): Promise { return this.createParticipantKeychain( MPCv2PartiesEnum.USER, @@ -585,7 +589,8 @@ export class EcdsaMPCv2Utils extends BaseEcdsaUtils { originalPasscodeEncryptionCode, webauthnInfo, encryptionSession, - encryptionVersion + encryptionVersion, + enterprise ); } diff --git a/modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsa.ts b/modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsa.ts index a439e47dae..5cad5b6b82 100644 --- a/modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsa.ts +++ b/modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsa.ts @@ -135,6 +135,7 @@ export class EddsaUtils extends baseTSSUtils { webauthnInfo, encryptionSession, encryptionVersion, + enterprise, }: CreateEddsaKeychainParams): Promise { const MPC = await Eddsa.initialize(); const bitgoKeyShares = bitgoKeychain.keyShares; @@ -202,17 +203,18 @@ export class EddsaUtils extends baseTSSUtils { } } if (webauthnInfo && userKeychainParams.encryptedPrv) { - userKeychainParams.webauthnDevices = [ - { - otpDeviceId: webauthnInfo.otpDeviceId, - prfSalt: webauthnInfo.prfSalt, - encryptedPrv: await this.bitgo.encryptAsync({ - input: JSON.stringify(userSigningMaterial), - password: webauthnInfo.passphrase, - encryptionVersion, - }), - }, - ]; + // Send the passkey as `webauthnInfo`; the deprecated `webauthnDevices` array is ignored by POST /key. + assert(enterprise, 'enterprise is required to attach a webauthn device to the user keychain'); + userKeychainParams.webauthnInfo = { + otpDeviceId: webauthnInfo.otpDeviceId, + prfSalt: webauthnInfo.prfSalt, + encryptedPrv: await this.bitgo.encryptAsync({ + input: JSON.stringify(userSigningMaterial), + password: webauthnInfo.passphrase, + encryptionVersion, + }), + enterpriseId: enterprise, + }; } return await this.baseCoin.keychains().add(userKeychainParams); @@ -412,6 +414,7 @@ export class EddsaUtils extends baseTSSUtils { webauthnInfo: params.webauthnInfo, encryptionSession, encryptionVersion: params.encryptionVersion, + enterprise: params.enterprise, }); const backupKeychainPromise = this.createBackupKeychain({ userGpgKey, diff --git a/modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsaMPCv2.ts b/modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsaMPCv2.ts index e746fbe282..70f29e81ab 100644 --- a/modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsaMPCv2.ts +++ b/modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsaMPCv2.ts @@ -195,7 +195,8 @@ export class EddsaMPCv2Utils extends BaseEddsaUtils { params.passphrase, params.originalPasscodeEncryptionCode, params.webauthnInfo, - params.encryptionVersion + params.encryptionVersion, + params.enterprise ); const backupKeychainPromise = this.addBackupKeychain( backupCommonKeychain, @@ -230,7 +231,8 @@ export class EddsaMPCv2Utils extends BaseEddsaUtils { passphrase?: string, originalPasscodeEncryptionCode?: string, webauthnInfo?: WebauthnKeyEncryptionInfo, - encryptionVersion?: EncryptionVersion + encryptionVersion?: EncryptionVersion, + enterprise?: string ): Promise { let source: string; let encryptedPrv: string | undefined = undefined; @@ -279,17 +281,18 @@ export class EddsaMPCv2Utils extends BaseEddsaUtils { }; if (webauthnInfo && participantIndex === MPCv2PartiesEnum.USER && privateMaterialBase64) { - keychainParams.webauthnDevices = [ - { - otpDeviceId: webauthnInfo.otpDeviceId, - prfSalt: webauthnInfo.prfSalt, - encryptedPrv: await this.bitgo.encryptAsync({ - input: privateMaterialBase64, - password: webauthnInfo.passphrase, - encryptionVersion, - }), - }, - ]; + // Send the passkey as `webauthnInfo`; the deprecated `webauthnDevices` array is ignored by POST /key. + assert(enterprise, 'enterprise is required to attach a webauthn device to the user keychain'); + keychainParams.webauthnInfo = { + otpDeviceId: webauthnInfo.otpDeviceId, + prfSalt: webauthnInfo.prfSalt, + encryptedPrv: await this.bitgo.encryptAsync({ + input: privateMaterialBase64, + password: webauthnInfo.passphrase, + encryptionVersion, + }), + enterpriseId: enterprise, + }; } const keychains = this.baseCoin.keychains(); @@ -303,7 +306,8 @@ export class EddsaMPCv2Utils extends BaseEddsaUtils { passphrase: string, originalPasscodeEncryptionCode?: string, webauthnInfo?: WebauthnKeyEncryptionInfo, - encryptionVersion?: EncryptionVersion + encryptionVersion?: EncryptionVersion, + enterprise?: string ): Promise { return this.createParticipantKeychain( MPCv2PartiesEnum.USER, @@ -313,7 +317,8 @@ export class EddsaMPCv2Utils extends BaseEddsaUtils { passphrase, originalPasscodeEncryptionCode, webauthnInfo, - encryptionVersion + encryptionVersion, + enterprise ); } From ae271f709e07a9f165a7bcc3b8ead4fdc9107a12 Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Fri, 22 May 2026 15:15:05 +0200 Subject: [PATCH 23/34] refactor(abstract-utxo): drop utxolib RootWalletKeys from keychains.ts Switch the instanceof check and Triple conversion path to use fixedScriptWallet.RootWalletKeys from wasm-utxo. The other input shapes (UtxoNamedKeychains, Triple<{pub}>, string[]) continue to work via the existing toKeychainTriple/string-array branches. Refs: T1-3279 --- modules/abstract-utxo/src/keychains.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/modules/abstract-utxo/src/keychains.ts b/modules/abstract-utxo/src/keychains.ts index 073b40a848..2903a556bf 100644 --- a/modules/abstract-utxo/src/keychains.ts +++ b/modules/abstract-utxo/src/keychains.ts @@ -1,7 +1,6 @@ import assert from 'assert'; import * as t from 'io-ts'; -import { bitgo } from '@bitgo/utxo-lib'; import { IRequestTracer, IWallet, KeyIndices, promiseProps, Triple } from '@bitgo/sdk-core'; import { BIP32, bip32, fixedScriptWallet } from '@bitgo/wasm-utxo'; @@ -48,10 +47,10 @@ export function toKeychainTriple(keychains: UtxoNamedKeychains): Triple | string[] + keychains: fixedScriptWallet.RootWalletKeys | UtxoNamedKeychains | Triple<{ pub: string }> | string[] ): Triple { - if (keychains instanceof bitgo.RootWalletKeys) { - return keychains.triple.map((k) => BIP32.fromBase58(k.toBase58())) as Triple; + if (keychains instanceof fixedScriptWallet.RootWalletKeys) { + return [keychains.userKey(), keychains.backupKey(), keychains.bitgoKey()]; } if (Array.isArray(keychains)) { if (keychains.length !== 3) { From 0b6356af6d8e80ffc923761c4d843213c553b8d9 Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Fri, 22 May 2026 15:16:33 +0200 Subject: [PATCH 24/34] refactor(abstract-utxo): drop unused exports from wasmUtil MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove the unused toWasmECPair / ECPairKey helper (no src callers) and inline the isUtxoLibPsbt type guard into toWasmPsbt's narrowing branch. The file remains a bridge between utxolib and wasm-utxo types — its remaining callers (offlineVault/OfflineVaultHalfSigned.ts, impl/btc/inscriptionBuilder.ts, transaction/descriptor/parse.ts, transaction/fixedScript/signPsbtWasm.ts) still pass utxolib instances. Refs: T1-3279 --- modules/abstract-utxo/src/wasmUtil.ts | 25 ++----------------------- 1 file changed, 2 insertions(+), 23 deletions(-) diff --git a/modules/abstract-utxo/src/wasmUtil.ts b/modules/abstract-utxo/src/wasmUtil.ts index f22e4fcf50..035ba82613 100644 --- a/modules/abstract-utxo/src/wasmUtil.ts +++ b/modules/abstract-utxo/src/wasmUtil.ts @@ -1,8 +1,7 @@ -import { BIP32, bip32, ECPair, Psbt, descriptorWallet } from '@bitgo/wasm-utxo'; +import { BIP32, bip32, Psbt, descriptorWallet } from '@bitgo/wasm-utxo'; import * as utxolib from '@bitgo/utxo-lib'; export type BIP32Key = BIP32 | bip32.BIP32Interface | utxolib.BIP32Interface; -export type ECPairKey = ECPair | utxolib.ECPairInterface | Uint8Array; export type UtxoLibPsbt = utxolib.Psbt | utxolib.bitgo.UtxoPsbt; /** @@ -36,23 +35,6 @@ export function toUtxolibBIP32(key: BIP32Key): utxolib.BIP32Interface { return utxolib.bip32.fromBase58(key.toBase58()); } -export function toWasmECPair(key: ECPairKey): ECPair { - if (key instanceof ECPair) { - return key; - } - if (key instanceof Uint8Array) { - return ECPair.from(key); - } - if (key.privateKey) { - return ECPair.fromPrivateKey(key.privateKey); - } - return ECPair.fromPublicKey(key.publicKey); -} - -export function isUtxoLibPsbt(psbt: unknown): psbt is UtxoLibPsbt { - return psbt instanceof utxolib.Psbt || psbt instanceof utxolib.bitgo.UtxoPsbt; -} - export function toWasmPsbt(psbt: Psbt | UtxoLibPsbt | Uint8Array): Psbt { if (psbt instanceof Psbt) { return psbt; @@ -60,10 +42,7 @@ export function toWasmPsbt(psbt: Psbt | UtxoLibPsbt | Uint8Array): Psbt { if (psbt instanceof Uint8Array) { return Psbt.deserialize(psbt); } - if (isUtxoLibPsbt(psbt)) { - return Psbt.deserialize(psbt.toBuffer()); - } - throw new Error('Unsupported PSBT type'); + return Psbt.deserialize(psbt.toBuffer()); } /** From 45f03387937f6d832e68133f51a169411b7c7ec3 Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Fri, 22 May 2026 15:56:27 +0200 Subject: [PATCH 25/34] refactor(abstract-utxo): delete wasmUtil.ts and drop utxolib from runtime deps All callers have been migrated off the wasmUtil bridge. Delete the file and move @bitgo/utxo-lib from dependencies to devDependencies in package.json. The dep is still required for tests (utxolib.testutil.AcidTest, network constants, BIP32 helpers). Refs: T1-3279 --- modules/abstract-utxo/package.json | 2 +- modules/abstract-utxo/src/wasmUtil.ts | 53 --------------------------- 2 files changed, 1 insertion(+), 54 deletions(-) delete mode 100644 modules/abstract-utxo/src/wasmUtil.ts diff --git a/modules/abstract-utxo/package.json b/modules/abstract-utxo/package.json index 6e0acbd80c..169fffdf81 100644 --- a/modules/abstract-utxo/package.json +++ b/modules/abstract-utxo/package.json @@ -65,7 +65,6 @@ "@bitgo/sdk-core": "^37.3.0", "@bitgo/utxo-core": "^1.39.0", "@bitgo/utxo-descriptors": "^1.3.0", - "@bitgo/utxo-lib": "^11.22.1", "@bitgo/utxo-ord": "^1.32.0", "@bitgo/wasm-utxo": "^4.16.0", "@types/lodash": "^4.14.121", @@ -78,6 +77,7 @@ }, "devDependencies": { "@bitgo/sdk-test": "^9.1.46", + "@bitgo/utxo-lib": "^11.22.1", "mocha": "^10.2.0" }, "gitHead": "18e460ddf02de2dbf13c2aa243478188fb539f0c" diff --git a/modules/abstract-utxo/src/wasmUtil.ts b/modules/abstract-utxo/src/wasmUtil.ts deleted file mode 100644 index 035ba82613..0000000000 --- a/modules/abstract-utxo/src/wasmUtil.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { BIP32, bip32, Psbt, descriptorWallet } from '@bitgo/wasm-utxo'; -import * as utxolib from '@bitgo/utxo-lib'; - -export type BIP32Key = BIP32 | bip32.BIP32Interface | utxolib.BIP32Interface; -export type UtxoLibPsbt = utxolib.Psbt | utxolib.bitgo.UtxoPsbt; - -/** - * Map of descriptor name to Descriptor instance. - * Re-exported from wasm-utxo for consistency. - */ -export type DescriptorMap = descriptorWallet.DescriptorMap; - -/** - * Key type accepted by descriptorWallet.signWithKey - */ -export type SignerKey = Parameters[1]; - -/** - * Convert a utxo-lib BIP32Interface to a wasm-utxo BIP32 instance. - * Preserves private key by using base58 serialization. - */ -export function toWasmBIP32(key: BIP32Key): BIP32 { - if (key instanceof BIP32) { - return key; - } - // All utxo-lib BIP32Interface instances have toBase58 - return BIP32.fromBase58(key.toBase58()); -} - -/** - * Convert a wasm-utxo BIP32 to a utxo-lib BIP32Interface. - * Used at boundaries where utxo-lib APIs require their own BIP32Interface type. - */ -export function toUtxolibBIP32(key: BIP32Key): utxolib.BIP32Interface { - return utxolib.bip32.fromBase58(key.toBase58()); -} - -export function toWasmPsbt(psbt: Psbt | UtxoLibPsbt | Uint8Array): Psbt { - if (psbt instanceof Psbt) { - return psbt; - } - if (psbt instanceof Uint8Array) { - return Psbt.deserialize(psbt); - } - return Psbt.deserialize(psbt.toBuffer()); -} - -/** - * Sum the `value` property of an array of objects. - */ -export function sumValues(arr: { value: bigint }[]): bigint { - return arr.reduce((sum, e) => sum + e.value, BigInt(0)); -} From 292a83be46ea67aea78105ffe275b74b62acedde Mon Sep 17 00:00:00 2001 From: neha-kri Date: Mon, 8 Jun 2026 20:36:21 +0530 Subject: [PATCH 26/34] fix(statics): reject duplicate contract address and NFT index keys in CoinMap TICKET: CGD-715 --- modules/statics/src/coins.ts | 14 +- modules/statics/src/coins/erc7984Tokens.ts | 4 +- modules/statics/src/errors.ts | 14 ++ modules/statics/src/map.ts | 61 +++++++- modules/statics/test/unit/coins.ts | 172 ++++++++++++++++++++- 5 files changed, 256 insertions(+), 9 deletions(-) diff --git a/modules/statics/src/coins.ts b/modules/statics/src/coins.ts index c7f49e2d30..2ba28d1abf 100644 --- a/modules/statics/src/coins.ts +++ b/modules/statics/src/coins.ts @@ -468,6 +468,10 @@ export function createTokenMapUsingConfigDetails(tokenConfigMap: Record): boolean { + if (coin instanceof ContractAddressDefinedToken) { + const key = CoinMap.contractAddressKey(coin); + const existing = this._coinByContractAddress.get(key); + return existing !== undefined && existing.network.type === coin.network.type; + } + if (coin instanceof NFTCollectionIdDefinedToken) { + const key = CoinMap.nftCollectionIdKey(coin); + const existing = this._coinByNftCollectionID.get(key); + return existing !== undefined && existing.network.type === coin.network.type; + } + return false; + } + static fromCoins(coins: Readonly[]): CoinMap { const coinMap = new CoinMap(); coins.forEach((coin) => { @@ -47,9 +82,25 @@ export class CoinMap { if (coin.isToken) { if (coin instanceof ContractAddressDefinedToken) { - this._coinByContractAddress.set(`${coin.family}:${coin.contractAddress}`, coin); + const contractAddressKey = CoinMap.contractAddressKey(coin); + const existingByContractAddress = this._coinByContractAddress.get(contractAddressKey); + if (existingByContractAddress) { + if (existingByContractAddress.network.type === coin.network.type) { + throw new DuplicateContractAddressDefinitionError(contractAddressKey, existingByContractAddress.name); + } + } else { + this._coinByContractAddress.set(contractAddressKey, coin); + } } else if (coin instanceof NFTCollectionIdDefinedToken) { - this._coinByNftCollectionID.set(`${coin.prefix}${coin.family}:${coin.nftCollectionId}`, coin); + const nftCollectionKey = CoinMap.nftCollectionIdKey(coin); + const existingByNftCollectionId = this._coinByNftCollectionID.get(nftCollectionKey); + if (existingByNftCollectionId) { + if (existingByNftCollectionId.network.type === coin.network.type) { + throw new DuplicateNftCollectionIdDefinitionError(nftCollectionKey, existingByNftCollectionId.name); + } + } else { + this._coinByNftCollectionID.set(nftCollectionKey, coin); + } } } } @@ -69,9 +120,9 @@ export class CoinMap { } if (oldCoin.isToken) { if (oldCoin instanceof ContractAddressDefinedToken) { - this._coinByContractAddress.delete(`${oldCoin.family}:${oldCoin.contractAddress}`); + this._coinByContractAddress.delete(CoinMap.contractAddressKey(oldCoin)); } else if (oldCoin instanceof NFTCollectionIdDefinedToken) { - this._coinByNftCollectionID.delete(`${oldCoin.prefix}${oldCoin.family}:${oldCoin.nftCollectionId}`); + this._coinByNftCollectionID.delete(CoinMap.nftCollectionIdKey(oldCoin)); } } } diff --git a/modules/statics/test/unit/coins.ts b/modules/statics/test/unit/coins.ts index 8ebbda4fa1..d310979516 100644 --- a/modules/statics/test/unit/coins.ts +++ b/modules/statics/test/unit/coins.ts @@ -41,7 +41,7 @@ import { trimmedDynamicBaseChainConfig, } from './resources/amsTokenConfig'; import { EthLikeErc20Token } from '../../../sdk-coin-evm/src'; -import { ProgramID } from '../../src/account'; +import { ProgramID, taptNFTCollection, terc20 } from '../../src/account'; import { allCoinsAndTokens } from '../../src/allCoinsAndTokens'; interface DuplicateCoinObject { @@ -753,6 +753,70 @@ describe('CoinMap', function () { (() => CoinMap.fromCoins([btc, btc2])).should.throw(`coin with id '${btc.id}' is already defined`); }); + it('should fail to map tokens with duplicated contract address for the same family', () => { + const template = coins.get('tusdc') as Erc20Coin; + const contractAddress = template.contractAddress.toString(); + const tokenA = terc20( + '11111111-1111-4111-8111-111111111111', + 'token-a', + 'Token A', + 6, + contractAddress, + template.asset, + template.features, + template.prefix, + template.suffix, + template.network as EthereumNetwork + ); + const tokenB = terc20( + '22222222-2222-4222-8222-222222222222', + 'token-b', + 'Token B', + 18, + contractAddress, + template.asset, + template.features, + template.prefix, + template.suffix, + template.network as EthereumNetwork + ); + const contractAddressKey = `${tokenA.family}:${contractAddress}`; + (() => CoinMap.fromCoins([tokenA, tokenB])).should.throw( + `token with contract address '${contractAddressKey}' is already defined as 'token-a'` + ); + }); + + it('should fail to map tokens with duplicated NFT collection id for the same family', () => { + const template = coins.get('tapt:nftcollection1'); + const nftCollectionId = '0xbbc561fbfa5d105efd8dfb06ae3e7e5be46331165b99d518f094c701e40603b5'; + const tokenA = taptNFTCollection( + '11111111-1111-4111-8111-111111111111', + 'tapt:nftcollection-a', + 'NFT Collection A', + nftCollectionId, + template.asset, + template.features, + template.prefix, + template.suffix, + template.network + ); + const tokenB = taptNFTCollection( + '22222222-2222-4222-8222-222222222222', + 'tapt:nftcollection-b', + 'NFT Collection B', + nftCollectionId, + template.asset, + template.features, + template.prefix, + template.suffix, + template.network + ); + const nftCollectionKey = `${tokenA.prefix}${tokenA.family}:${nftCollectionId}`; + (() => CoinMap.fromCoins([tokenA, tokenB])).should.throw( + `token with NFT collection id '${nftCollectionKey}' is already defined as 'tapt:nftcollection-a'` + ); + }); + it('should have iterator', function () { [...coins].length.should.be.greaterThan(100); }); @@ -1447,6 +1511,112 @@ describe('create token map using config details', () => { }); }); +describe('create token map contract address de-duplication', () => { + function firstStaticErc20(): Readonly { + for (const [, coin] of coins) { + if (coin instanceof Erc20Coin) { + return coin as Readonly; + } + } + throw new Error('expected at least one static ERC20 token in the coin map'); + } + + function collidingAmsConfig( + staticToken: Readonly, + name: string, + id: string + ): Parameters[0] { + return { + [name]: [ + { + id, + fullName: 'Colliding AMS Token', + name, + prefix: '', + suffix: name.toUpperCase(), + baseUnit: 'wei', + kind: 'crypto', + family: staticToken.family, + isToken: true, + features: [...staticToken.features], + decimalPlaces: staticToken.decimalPlaces, + asset: name, + network: staticToken.network, + primaryKeyCurve: 'secp256k1', + contractAddress: staticToken.contractAddress, + }, + ], + } as unknown as Parameters[0]; + } + + const collidingName = 'eth:cshld976colliding'; + const collidingId = 'aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee'; + + it('uses a name and id not already in the static coin map', () => { + coins.has(collidingName).should.eql(false); + coins.has(collidingId).should.eql(false); + }); + + it('skips an AMS token that reuses an existing static contract address under a different name', () => { + const staticToken = firstStaticErc20(); + const config = collidingAmsConfig(staticToken, collidingName, collidingId); + + let tokenMap: CoinMap | undefined; + (() => { + tokenMap = createTokenMapUsingConfigDetails(config); + }).should.not.throw(); + + (tokenMap as CoinMap).has(collidingName).should.eql(false); + (tokenMap as CoinMap).has(staticToken.name).should.eql(true); + }); + + it('skips second AMS token that reuses a contract address already claimed by a first AMS token', () => { + const staticToken = firstStaticErc20(); + const sharedContractAddress = '0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef'; + const amsNameA = 'eth:ams-only-a-976'; + const amsIdA = 'cccccccc-dddd-4eee-8fff-000000000001'; + const amsNameB = 'eth:ams-only-b-976'; + const amsIdB = 'cccccccc-dddd-4eee-8fff-000000000002'; + + const makeConfig = (name: string, id: string): Parameters[0] => + ({ + [name]: [ + { + id, + fullName: 'AMS-only Token', + name, + prefix: '', + suffix: name.toUpperCase(), + baseUnit: 'wei', + kind: 'crypto', + family: staticToken.family, + isToken: true, + features: [...staticToken.features], + decimalPlaces: staticToken.decimalPlaces, + asset: name, + network: staticToken.network, + primaryKeyCurve: 'secp256k1', + contractAddress: sharedContractAddress, + }, + ], + } as unknown as Parameters[0]); + + const config = { ...makeConfig(amsNameA, amsIdA), ...makeConfig(amsNameB, amsIdB) }; + + let tokenMap: CoinMap | undefined; + (() => { + tokenMap = createTokenMapUsingConfigDetails(config); + }).should.not.throw(); + + const map = tokenMap as CoinMap; + const hasA = map.has(amsNameA); + const hasB = map.has(amsNameB); + // Exactly one of the two AMS tokens wins; the second with the same address is skipped. + (hasA || hasB).should.eql(true, 'first AMS token should be accepted'); + (hasA && hasB).should.eql(false, 'second AMS token with duplicate address should be skipped'); + }); +}); + describe('DynamicCoin and dynamic base chain support', function () { describe('createToken with dynamic base chain', function () { it('should return a DynamicCoin when isToken is false with a BaseNetwork instance', function () { From cb631061486289f3ce1ad237a984eddcf3e8a0e0 Mon Sep 17 00:00:00 2001 From: rajangarg047 Date: Wed, 10 Jun 2026 14:18:26 -0400 Subject: [PATCH 27/34] fix(sdk-core): persist passkey for onchain generateWallet Switch the onchain generateWallet user-keychain path to send `webauthnInfo` (single object, with enterpriseId threaded from params.enterprise) instead of the deprecated `webauthnDevices` array. The backend's atomic POST /key only consumes `webauthnInfo` and ignores `webauthnDevices` on input, so the passkey was never persisted for onchain hot wallets created via generateWallet. Update walletsWebauthn unit tests to assert webauthnInfo (with enterpriseId) instead of webauthnDevices. Ticket: WCN-870 Co-Authored-By: Claude Opus 4.8 (1M context) --- modules/sdk-core/src/bitgo/wallet/wallets.ts | 23 ++++++++-------- .../test/unit/bitgo/wallet/walletsWebauthn.ts | 26 +++++++++++-------- 2 files changed, 27 insertions(+), 22 deletions(-) diff --git a/modules/sdk-core/src/bitgo/wallet/wallets.ts b/modules/sdk-core/src/bitgo/wallet/wallets.ts index c53bde3b1f..b6febbc929 100644 --- a/modules/sdk-core/src/bitgo/wallet/wallets.ts +++ b/modules/sdk-core/src/bitgo/wallet/wallets.ts @@ -606,18 +606,19 @@ export class Wallets implements IWallets { // If WebAuthn info is provided, store an additional copy of the private key encrypted // with the PRF-derived passphrase so the authenticator can later decrypt it. + // Send it as `webauthnInfo` (single object, with enterpriseId) so the atomic + // POST /key persists the passkey — the backend ignores the deprecated `webauthnDevices`. if (params.webauthnInfo && userKeychain.prv) { - userKeychainParams.webauthnDevices = [ - { - otpDeviceId: params.webauthnInfo.otpDeviceId, - prfSalt: params.webauthnInfo.prfSalt, - encryptedPrv: await this.bitgo.encryptAsync({ - password: params.webauthnInfo.passphrase, - input: userKeychain.prv, - encryptionVersion: params.encryptionVersion, - }), - }, - ]; + userKeychainParams.webauthnInfo = { + otpDeviceId: params.webauthnInfo.otpDeviceId, + prfSalt: params.webauthnInfo.prfSalt, + enterpriseId: params.enterprise, + encryptedPrv: await this.bitgo.encryptAsync({ + password: params.webauthnInfo.passphrase, + input: userKeychain.prv, + encryptionVersion: params.encryptionVersion, + }), + }; } } diff --git a/modules/sdk-core/test/unit/bitgo/wallet/walletsWebauthn.ts b/modules/sdk-core/test/unit/bitgo/wallet/walletsWebauthn.ts index 3167ee9e73..785a736849 100644 --- a/modules/sdk-core/test/unit/bitgo/wallet/walletsWebauthn.ts +++ b/modules/sdk-core/test/unit/bitgo/wallet/walletsWebauthn.ts @@ -65,7 +65,7 @@ describe('Wallets - WebAuthn wallet creation', function () { }); describe('generateWallet with webauthnInfo', function () { - it('should add webauthnDevices to keychain params when webauthnInfo is provided', async function () { + it('should add webauthnInfo (with enterpriseId) to keychain params when webauthnInfo is provided', async function () { const webauthnInfo = { otpDeviceId: 'device-123', prfSalt: 'salt-abc', @@ -75,16 +75,18 @@ describe('Wallets - WebAuthn wallet creation', function () { await wallets.generateWallet({ label: 'Test Wallet', passphrase: 'wallet-passphrase', + enterprise: 'enterprise-123', webauthnInfo, }); assert.strictEqual(mockKeychains.add.calledOnce, true); const addParams = mockKeychains.add.firstCall.args[0]; - addParams.should.have.property('webauthnDevices'); - addParams.webauthnDevices.should.have.length(1); - addParams.webauthnDevices[0].should.have.property('otpDeviceId', webauthnInfo.otpDeviceId); - addParams.webauthnDevices[0].should.have.property('prfSalt', webauthnInfo.prfSalt); - addParams.webauthnDevices[0].should.have.property('encryptedPrv'); + addParams.should.not.have.property('webauthnDevices'); + addParams.should.have.property('webauthnInfo'); + addParams.webauthnInfo.should.have.property('otpDeviceId', webauthnInfo.otpDeviceId); + addParams.webauthnInfo.should.have.property('prfSalt', webauthnInfo.prfSalt); + addParams.webauthnInfo.should.have.property('enterpriseId', 'enterprise-123'); + addParams.webauthnInfo.should.have.property('encryptedPrv'); }); it('should encrypt user private key with the webauthn passphrase', async function () { @@ -102,7 +104,7 @@ describe('Wallets - WebAuthn wallet creation', function () { const addParams = mockKeychains.add.firstCall.args[0]; const expectedEncryptedPrv = `encrypted:${webauthnPassphrase}:${userPrv}`; - addParams.webauthnDevices[0].should.have.property('encryptedPrv', expectedEncryptedPrv); + addParams.webauthnInfo.should.have.property('encryptedPrv', expectedEncryptedPrv); }); it('should also encrypt user private key with wallet passphrase when webauthnInfo is provided', async function () { @@ -143,7 +145,7 @@ describe('Wallets - WebAuthn wallet creation', function () { passwordsUsed.should.containEql(webauthnPassphrase); }); - it('should not add webauthnDevices when webauthnInfo is not provided', async function () { + it('should not add webauthnInfo when webauthnInfo is not provided', async function () { await wallets.generateWallet({ label: 'Test Wallet', passphrase: 'wallet-passphrase', @@ -151,11 +153,12 @@ describe('Wallets - WebAuthn wallet creation', function () { assert.strictEqual(mockKeychains.add.calledOnce, true); const addParams = mockKeychains.add.firstCall.args[0]; + addParams.should.not.have.property('webauthnInfo'); addParams.should.not.have.property('webauthnDevices'); }); - it('should not add webauthnDevices when userKey is explicitly provided (no prv available)', async function () { - // When a user-provided public key is used, there is no private key to encrypt, so webauthnDevices is skipped + it('should not add webauthnInfo when userKey is explicitly provided (no prv available)', async function () { + // When a user-provided public key is used, there is no private key to encrypt, so webauthnInfo is skipped await wallets.generateWallet({ label: 'Test Wallet', userKey: userPub, @@ -167,10 +170,11 @@ describe('Wallets - WebAuthn wallet creation', function () { }, }); - // add is called for both user keychain (pub-only) and backup keychain - neither should have webauthnDevices + // add is called for both user keychain (pub-only) and backup keychain - neither should have webauthnInfo const allAddCalls = mockKeychains.add.getCalls(); assert.ok(allAddCalls.length > 0, 'expected keychains().add to be called at least once'); for (const call of allAddCalls) { + call.args[0].should.not.have.property('webauthnInfo'); call.args[0].should.not.have.property('webauthnDevices'); } }); From 298cb0aba20fe98cdc22f87c2bfdb78b06d1c797 Mon Sep 17 00:00:00 2001 From: Wayne Wen Date: Wed, 10 Jun 2026 17:47:48 -0400 Subject: [PATCH 28/34] ci: flag internal-info leaks in PR review prompt Ticket: WCN-850 --- .github/prompts/code-review.md | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/.github/prompts/code-review.md b/.github/prompts/code-review.md index 9ca6fb279c..4651cfd505 100644 --- a/.github/prompts/code-review.md +++ b/.github/prompts/code-review.md @@ -8,6 +8,15 @@ You are an expert code reviewer for the BitGoJS cryptocurrency wallet SDK. Pleas - Proper validation of transaction parameters - Safe handling of private keys and sensitive data +## Internal Information Leakage (Public Repository) +Comments and strings should describe what the code does, not the dev process. Flag in comments, JSDoc, test names, and error/log strings: +- Verification/testing metadata (dates, "dry-run confirmed", "verified/tested on", investigation notes) +- Internal team/system names or codenames (e.g. "by WP"), infra, or tooling +- Internal ticket IDs or links to internal-only docs +- Rationale on how/why a change was made rather than code behavior + +For each, suggest a behavior-only rewrite. + ## Code Quality & Architecture - Adherence to BitGoJS coding standards and patterns - TypeScript type safety and interface compliance @@ -35,8 +44,9 @@ You are an expert code reviewer for the BitGoJS cryptocurrency wallet SDK. Pleas Please provide constructive feedback focusing on: 1. Critical issues that must be addressed -2. Suggestions for improvement -3. Questions about design decisions -4. Acknowledgment of good practices +2. Internal-information leaks in comments or strings (must be removed before merge) +3. Suggestions for improvement +4. Questions about design decisions +5. Acknowledgment of good practices Be thorough but concise, and explain the reasoning behind your suggestions. From 477ffdc55f6b0560e2efcacf44a7be5a088af01c Mon Sep 17 00:00:00 2001 From: ArunBala-Bitgo Date: Thu, 11 Jun 2026 11:54:31 +0530 Subject: [PATCH 29/34] fix(statics): correct FLRP P-chain explorer transaction URLs Update FlareP and FlarePTestnet explorerUrl paths from /blockchain/pvm/transactions to /blockchain/pvm/tx/ so transaction hash links resolve correctly on Flarescan. Ticket: CECHO-1177 Co-authored-by: Cursor --- modules/statics/src/networks.ts | 4 ++-- modules/statics/test/unit/networks.ts | 11 +++++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/modules/statics/src/networks.ts b/modules/statics/src/networks.ts index fbed8a2a60..30bfa42737 100644 --- a/modules/statics/src/networks.ts +++ b/modules/statics/src/networks.ts @@ -2297,7 +2297,7 @@ export class FlareP extends Mainnet implements FlareNetwork { assetId = 'Flare'; name = 'FlareP'; family = CoinFamily.FLRP; - explorerUrl = 'https://flarescan.com/blockchain/pvm/transactions/'; + explorerUrl = 'https://flarescan.com/blockchain/pvm/tx/'; accountExplorerUrl = 'https://flarescan.com/blockchain/pvm/address/'; blockchainID = '11111111111111111111111111111111LpoYY'; cChainBlockchainID = '2q9e4r6Mu3U68nU1fYjgbR6JvwrRx36CohpAX5UQxse55x1Q5'; @@ -2331,7 +2331,7 @@ export class FlareP extends Mainnet implements FlareNetwork { export class FlarePTestnet extends Testnet implements FlareNetwork { name = 'FlarePTestnet'; family = CoinFamily.FLRP; - explorerUrl = 'https://coston2.testnet.flarescan.com/blockchain/pvm/transactions'; + explorerUrl = 'https://coston2.testnet.flarescan.com/blockchain/pvm/tx/'; accountExplorerUrl = 'https://coston2.testnet.flarescan.com/blockchain/pvm/address/'; flarePublicUrl = 'https://coston2.testnet.flare.network'; blockchainID = '11111111111111111111111111111111LpoYY'; diff --git a/modules/statics/test/unit/networks.ts b/modules/statics/test/unit/networks.ts index 2b92f1d3fb..c859bd45d5 100644 --- a/modules/statics/test/unit/networks.ts +++ b/modules/statics/test/unit/networks.ts @@ -70,6 +70,17 @@ Object.entries(Networks).forEach(([category, networks]) => { Networks.test.near.accountExplorerUrl.should.equal('https://testnet.nearblocks.io/address/'); }); }); + + describe('FlareP Network', function () { + it('should have correct explorer URLs', function () { + Networks.main.flrP.explorerUrl.should.equal('https://flarescan.com/blockchain/pvm/tx/'); + Networks.main.flrP.accountExplorerUrl.should.equal('https://flarescan.com/blockchain/pvm/address/'); + Networks.test.flrP.explorerUrl.should.equal('https://coston2.testnet.flarescan.com/blockchain/pvm/tx/'); + Networks.test.flrP.accountExplorerUrl.should.equal( + 'https://coston2.testnet.flarescan.com/blockchain/pvm/address/' + ); + }); + }); }); }); From 7ace4598f7bb2fb21eefb9a6c617de993baeb961 Mon Sep 17 00:00:00 2001 From: Hitansh Madan Date: Thu, 11 Jun 2026 11:27:38 +0530 Subject: [PATCH 30/34] feat(sdk-core): disable active operation pre-flight check in DefiVault Comment out the pre-flight check that queries the defi-service for active operations, as the endpoint is not yet deployed. Added TODO(CGD-1709) to re-enable once the defi-service operations endpoint is live. CGD-1533 Co-Authored-By: Claude Opus 4.6 --- modules/sdk-core/src/bitgo/defi/defiVault.ts | 22 ++++++++++--------- .../test/unit/bitgo/defi/defiVault.ts | 17 ++------------ 2 files changed, 14 insertions(+), 25 deletions(-) diff --git a/modules/sdk-core/src/bitgo/defi/defiVault.ts b/modules/sdk-core/src/bitgo/defi/defiVault.ts index 150d1d44ad..63071f38f3 100644 --- a/modules/sdk-core/src/bitgo/defi/defiVault.ts +++ b/modules/sdk-core/src/bitgo/defi/defiVault.ts @@ -52,7 +52,8 @@ export class DefiVault implements IDefiVault { * * Internally issues two sendMany calls (approve + deposit) and returns the * operationId that links them. If the deposit sendMany fails after - * the approve succeeds, the approve is auto-cancelled (fail-fast). + * the approve succeeds, the error propagates — the server-side reconciler + * handles orphaned approvals. * * @param params.vaultId - DeFi-service vault identifier * @param params.amount - amount in base units of the underlying asset @@ -67,15 +68,16 @@ export class DefiVault implements IDefiVault { throw new Error('amount is required'); } - // Layer-1 pre-flight: reject if an active deposit already exists for this (wallet, vault) - const activeOps: DefiOperationListResult = await this.bitgo - .get(this.bitgo.microservicesUrl(this.operationsUrl())) - .query({ vaultId: params.vaultId, state: 'active' }) - .result(); - - if (activeOps.items && activeOps.items.length > 0) { - throw new ActiveOperationExistsError(activeOps.items[0].operationId); - } + // TODO(CGD-1709): Re-enable active operation pre-flight check once the + // defi-service operations endpoint is deployed and returning active state. + // const activeOps: DefiOperationListResult = await this.bitgo + // .get(this.bitgo.microservicesUrl(this.operationsUrl())) + // .query({ vaultId: params.vaultId, state: 'active' }) + // .result(); + // + // if (activeOps.items && activeOps.items.length > 0) { + // throw new ActiveOperationExistsError(activeOps.items[0].operationId); + // } // Step 1: Approve txRequest via sendMany const approveResult = await this.wallet.sendMany({ diff --git a/modules/sdk-core/test/unit/bitgo/defi/defiVault.ts b/modules/sdk-core/test/unit/bitgo/defi/defiVault.ts index c4c810d06b..6a4377c920 100644 --- a/modules/sdk-core/test/unit/bitgo/defi/defiVault.ts +++ b/modules/sdk-core/test/unit/bitgo/defi/defiVault.ts @@ -54,10 +54,6 @@ describe('DefiVault', function () { it('should call sendMany for approve and deposit on happy path', async function () { const operationId = 'op-uuid-123'; - // Pre-flight: no active operations - const preflightReq = mockRequest({ items: [] }); - mockBitGo.get.onFirstCall().returns(preflightReq); - // Mock sendMany for approve and deposit const sendManyStub = sinon.stub(wallet, 'sendMany'); sendManyStub.onFirstCall().resolves({ @@ -82,9 +78,6 @@ describe('DefiVault', function () { result.txRequestIds.approve.should.equal('txreq-approve-1'); result.txRequestIds.deposit.should.equal('txreq-deposit-1'); - // Verify pre-flight was called with correct query - preflightReq.query.calledWith({ vaultId: 'vlt-galaxy-usdc', state: 'active' }).should.be.true(); - // Verify sendMany was called with correct params for approve sendManyStub.calledTwice.should.be.true(); const approveArgs: any = sendManyStub.firstCall.args[0]; @@ -98,7 +91,8 @@ describe('DefiVault', function () { depositArgs.defiParams.operationId.should.equal(operationId); }); - it('should reject when an active operation already exists', async function () { + // TODO(CGD-1709): Re-enable when active operation pre-flight check is restored + xit('should reject when an active operation already exists', async function () { const preflightReq = mockRequest({ items: [{ operationId: 'existing-op-id', state: 'APPROVE_TX_REQUESTED' }], }); @@ -117,10 +111,6 @@ describe('DefiVault', function () { it('should propagate deposit sendMany failure without cleanup', async function () { const operationId = 'op-uuid-456'; - // Pre-flight: no active operations - const preflightReq = mockRequest({ items: [] }); - mockBitGo.get.returns(preflightReq); - // Mock sendMany: approve succeeds, deposit fails const sendManyStub = sinon.stub(wallet, 'sendMany'); sendManyStub.onFirstCall().resolves({ @@ -154,9 +144,6 @@ describe('DefiVault', function () { it('should pass clientIdempotencyKey and walletPassphrase when provided', async function () { const operationId = 'op-uuid-789'; - const preflightReq = mockRequest({ items: [] }); - mockBitGo.get.returns(preflightReq); - const sendManyStub = sinon.stub(wallet, 'sendMany'); sendManyStub.onFirstCall().resolves({ txRequest: { From dbbec427ac7325bd19dbedaaa9a24440eb99db42 Mon Sep 17 00:00:00 2001 From: Ravi Hegde Date: Thu, 11 Jun 2026 13:26:51 +0530 Subject: [PATCH 31/34] feat(statics): rename canton fullname in test & prod env Ticket: CHALO-581 --- modules/statics/src/allCoinsAndTokens.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/statics/src/allCoinsAndTokens.ts b/modules/statics/src/allCoinsAndTokens.ts index 9e4838bb36..0d90d8904c 100644 --- a/modules/statics/src/allCoinsAndTokens.ts +++ b/modules/statics/src/allCoinsAndTokens.ts @@ -3632,7 +3632,7 @@ export const allCoinsAndTokens = [ canton( '07385320-5a4f-48e9-97a5-86d4be9f24b0', 'canton', - 'Canton Coin', + 'Canton', Networks.main.canton, UnderlyingAsset.CANTON, [...CANTON_FEATURES, CoinFeature.UNSPENT_MODEL, CoinFeature.MERGE_UTXOS], @@ -3641,7 +3641,7 @@ export const allCoinsAndTokens = [ canton( 'f5d7f76b-fc5a-4da8-b1d0-a86ad0fd269e', 'tcanton', - 'Testnet Canton Coin', + 'Testnet Canton', Networks.test.canton, UnderlyingAsset.CANTON, [...CANTON_FEATURES, CoinFeature.UNSPENT_MODEL, CoinFeature.MERGE_UTXOS, CoinFeature.FANOUT_UTXOS], From 6f72724dd68be6e8f51c9e9a165ec7c9e3fb7f6d Mon Sep 17 00:00:00 2001 From: Dhanush GM Date: Thu, 11 Jun 2026 13:15:43 +0530 Subject: [PATCH 32/34] feat(statics): onboard polygon, avaxc, tempo Ticket: CSHLD-1024 --- modules/statics/src/allCoinsAndTokens.ts | 14 ++++++++++++++ modules/statics/src/base.ts | 3 +++ modules/statics/src/coins/avaxTokens.ts | 8 ++++++++ modules/statics/src/coins/ofcCoins.ts | 15 +++++++++++++++ modules/statics/src/coins/polygonTokens.ts | 9 +++++++++ 5 files changed, 49 insertions(+) diff --git a/modules/statics/src/allCoinsAndTokens.ts b/modules/statics/src/allCoinsAndTokens.ts index 9e4838bb36..0872588ffc 100644 --- a/modules/statics/src/allCoinsAndTokens.ts +++ b/modules/statics/src/allCoinsAndTokens.ts @@ -3544,6 +3544,20 @@ export const allCoinsAndTokens = [ CoinFeature.EVM_UNSIGNED_SWEEP_RECOVERY, ] ), + tip20Token( + '9dd63f8e-3f35-4d10-a623-fe7358ad66a4', + 'tempo:usdt0', + 'USDT0', + 6, + '0x20c00000000000000000000014f22ca97301eb73', + UnderlyingAsset['tempo:usdt0'], + [ + ...TEMPO_FEATURES, + CoinFeature.STABLECOIN, + CoinFeature.EVM_NON_BITGO_RECOVERY, + CoinFeature.EVM_UNSIGNED_SWEEP_RECOVERY, + ] + ), // Tempo TIP20 testnet tokens ttip20Token( 'e1872fd8-14ee-4dc9-bc5e-fd52552d9c60', diff --git a/modules/statics/src/base.ts b/modules/statics/src/base.ts index 16a5db6e7d..2766be0ca1 100644 --- a/modules/statics/src/base.ts +++ b/modules/statics/src/base.ts @@ -2823,6 +2823,7 @@ export enum UnderlyingAsset { 'avaxc:emdx' = 'avaxc:emdx', 'avaxc:eurc' = 'avaxc:eurc', 'avaxc:ausd' = 'avaxc:ausd', + 'avaxc:mxnd' = 'avaxc:mxnd', // End FTX missing AVAXC tokens // polygon Token ERC-20 @@ -2982,6 +2983,7 @@ export enum UnderlyingAsset { 'polygon:vio0' = 'polygon:vio0', 'polygon:wots0' = 'polygon:wots0', 'polygon:mext' = 'polygon:mext', + 'polygon:mxnd' = 'polygon:mxnd', 'polygon:pert' = 'polygon:pert', 'polygon:colt' = 'polygon:colt', 'polygon:bolt' = 'polygon:bolt', @@ -4058,6 +4060,7 @@ export enum UnderlyingAsset { 'tempo:pathusd' = 'tempo:pathusd', 'tempo:usdc' = 'tempo:usdc', 'tempo:usd1' = 'tempo:usd1', + 'tempo:usdt0' = 'tempo:usdt0', // Tempo testnet tokens 'ttempo:pathusd' = 'ttempo:pathusd', diff --git a/modules/statics/src/coins/avaxTokens.ts b/modules/statics/src/coins/avaxTokens.ts index fd45955ae9..cdb07f69a7 100644 --- a/modules/statics/src/coins/avaxTokens.ts +++ b/modules/statics/src/coins/avaxTokens.ts @@ -752,6 +752,14 @@ export const avaxTokens = [ UnderlyingAsset['avaxc:frnt'], [...AccountCoin.DEFAULT_FEATURES, CoinFeature.STABLECOIN] ), + avaxErc20( + '7efffcb6-b961-428b-9eed-41cea87e97b8', + 'avaxc:mxnd', + 'Mexican Digital Peso', + 6, + '0x6122c5964d39416c3f34475d00ceb37f50700314', + UnderlyingAsset['avaxc:mxnd'] + ), // End FTX missing AVAXC tokens tavaxErc20( 'cd107316-6e78-4936-946f-70e8fd5d8040', diff --git a/modules/statics/src/coins/ofcCoins.ts b/modules/statics/src/coins/ofcCoins.ts index 0493d2edac..d01c86448b 100644 --- a/modules/statics/src/coins/ofcCoins.ts +++ b/modules/statics/src/coins/ofcCoins.ts @@ -2559,6 +2559,13 @@ export const ofcCoins = [ undefined, [CoinFeature.STABLECOIN] ), + ofcAvaxErc20( + '475bfba5-3fd2-4f3c-8f0c-bb740e722f4d', + 'ofcavaxc:mxnd', + 'Mexican Digital Peso', + 6, + UnderlyingAsset['avaxc:mxnd'] + ), ofcAvaxErc20('9fb77e47-8916-4dcb-ac10-e11fa07172fb', 'ofcavaxc:nxpc', 'NEXPACE', 18, UnderlyingAsset['avaxc:nxpc']), ofcOpethErc20('10259b23-2e2e-4574-b146-b49f1119600f', 'ofcopeth:op', 'Optimism', 18, UnderlyingAsset['opeth:op']), ofcOpethErc20( @@ -3380,6 +3387,13 @@ export const ofcCoins = [ 6, UnderlyingAsset['polygon:xsgd'] ), + ofcPolygonErc20( + '76d1f5d9-b848-4a06-b541-bbb4c86dcfad', + 'ofcpolygon:mxnd', + 'Mexican Digital Peso', + 6, + UnderlyingAsset['polygon:mxnd'] + ), ofcPolygonErc20( '26eda2a9-0559-4f18-9bb7-547c2682b742', 'ofcpolygon:treta', @@ -5090,6 +5104,7 @@ export const ofcCoins = [ ), ofcTempoToken('c9a90ee0-6546-413c-9cbe-94fdc14985c5', 'ofctempo:usdc', 'USDC', 6, UnderlyingAsset['tempo:usdc']), ofcTempoToken('05ac1283-5e72-4cba-8b0f-38cbd23a25c6', 'ofctempo:usd1', 'USD1', 6, UnderlyingAsset['tempo:usd1']), + ofcTempoToken('554f9084-4ac8-466a-8675-3de33ffd47d7', 'ofctempo:usdt0', 'USDT0', 6, UnderlyingAsset['tempo:usdt0']), // Tempo testnet OFC tokens tofcTempoToken( '7912e76e-5a5c-4f1b-86e9-1fc2a51f5a98', diff --git a/modules/statics/src/coins/polygonTokens.ts b/modules/statics/src/coins/polygonTokens.ts index bf515c9bb2..81cbff2a41 100644 --- a/modules/statics/src/coins/polygonTokens.ts +++ b/modules/statics/src/coins/polygonTokens.ts @@ -1181,6 +1181,15 @@ export const polygonTokens = [ UnderlyingAsset['polygon:mext'], POLYGON_TOKEN_FEATURES_EXCLUDE_SINGAPORE ), + polygonErc20( + 'eee6f253-4490-46cd-92b3-59d8288a46a2', + 'polygon:mxnd', + 'Mexican Digital Peso', + 6, + '0xa4a4fcb23ffcd964346d2e4ecdf5a8c15c69b219', + UnderlyingAsset['polygon:mxnd'], + POLYGON_TOKEN_FEATURES + ), polygonErc20( 'cb393313-9ce5-44aa-aec0-f07c0ab13d76', 'polygon:pert', From e80d7ec2fc25bb5165f6dd1fc6df1efaf7a662b0 Mon Sep 17 00:00:00 2001 From: "B.Prasad" Date: Thu, 11 Jun 2026 14:48:47 +0530 Subject: [PATCH 33/34] chore(statics): updated coin features for bobaeth CECHO-305 TICKET: CHO-305 --- modules/statics/src/allCoinsAndTokens.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/modules/statics/src/allCoinsAndTokens.ts b/modules/statics/src/allCoinsAndTokens.ts index 9e4838bb36..25a14db5a2 100644 --- a/modules/statics/src/allCoinsAndTokens.ts +++ b/modules/statics/src/allCoinsAndTokens.ts @@ -2680,11 +2680,14 @@ export const allCoinsAndTokens = [ BaseUnit.ETH, [ ...EVM_FEATURES, + CoinFeature.EVM_COMPATIBLE_WP, CoinFeature.SHARED_EVM_SIGNING, CoinFeature.SHARED_EVM_SDK, CoinFeature.EVM_COMPATIBLE_IMS, CoinFeature.EVM_COMPATIBLE_UI, CoinFeature.SUPPORTS_ERC20, + CoinFeature.EVM_NON_BITGO_RECOVERY, + CoinFeature.EVM_UNSIGNED_SWEEP_RECOVERY, ] ), account( @@ -2697,11 +2700,14 @@ export const allCoinsAndTokens = [ BaseUnit.ETH, [ ...EVM_FEATURES, + CoinFeature.EVM_COMPATIBLE_WP, CoinFeature.SHARED_EVM_SIGNING, CoinFeature.SHARED_EVM_SDK, CoinFeature.EVM_COMPATIBLE_IMS, CoinFeature.EVM_COMPATIBLE_UI, CoinFeature.SUPPORTS_ERC20, + CoinFeature.EVM_NON_BITGO_RECOVERY, + CoinFeature.EVM_UNSIGNED_SWEEP_RECOVERY, ] ), From 467b3964881ca5daa55112c057ffa5d98676484c Mon Sep 17 00:00:00 2001 From: Prajwal U Date: Thu, 11 Jun 2026 17:58:31 +0530 Subject: [PATCH 34/34] chore(sdk-coin-ton): rename display name from Ton to Gram Ticket: CSHLD-1028 --- modules/sdk-coin-ton/src/ton.ts | 2 +- modules/sdk-coin-ton/src/tton.ts | 2 +- modules/sdk-coin-ton/test/unit/ton.ts | 4 ++-- modules/statics/src/allCoinsAndTokens.ts | 4 ++-- modules/statics/src/coins/ofcCoins.ts | 4 ++-- modules/statics/src/networks.ts | 4 ++-- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/modules/sdk-coin-ton/src/ton.ts b/modules/sdk-coin-ton/src/ton.ts index 608c7c8d6d..4abdee90e7 100644 --- a/modules/sdk-coin-ton/src/ton.ts +++ b/modules/sdk-coin-ton/src/ton.ts @@ -76,7 +76,7 @@ export class Ton extends BaseCoin { } public getFullName(): string { - return 'Ton'; + return 'Gram'; } /** @inheritDoc */ diff --git a/modules/sdk-coin-ton/src/tton.ts b/modules/sdk-coin-ton/src/tton.ts index dbe1b75e00..6d5619ebfa 100644 --- a/modules/sdk-coin-ton/src/tton.ts +++ b/modules/sdk-coin-ton/src/tton.ts @@ -34,6 +34,6 @@ export class Tton extends Ton { * Complete human-readable name of this coin */ public getFullName(): string { - return 'Testnet Ton'; + return 'Testnet Gram'; } } diff --git a/modules/sdk-coin-ton/test/unit/ton.ts b/modules/sdk-coin-ton/test/unit/ton.ts index 862514851f..41e9215ea6 100644 --- a/modules/sdk-coin-ton/test/unit/ton.ts +++ b/modules/sdk-coin-ton/test/unit/ton.ts @@ -60,12 +60,12 @@ describe('TON:', function () { ton.getChain().should.equal('ton'); ton.getFamily().should.equal('ton'); - ton.getFullName().should.equal('Ton'); + ton.getFullName().should.equal('Gram'); ton.getBaseFactor().should.equal(1e9); tton.getChain().should.equal('tton'); tton.getFamily().should.equal('ton'); - tton.getFullName().should.equal('Testnet Ton'); + tton.getFullName().should.equal('Testnet Gram'); tton.getBaseFactor().should.equal(1e9); }); diff --git a/modules/statics/src/allCoinsAndTokens.ts b/modules/statics/src/allCoinsAndTokens.ts index 9e4838bb36..fb9d51ae6d 100644 --- a/modules/statics/src/allCoinsAndTokens.ts +++ b/modules/statics/src/allCoinsAndTokens.ts @@ -1418,7 +1418,7 @@ export const allCoinsAndTokens = [ account( 'b5ba2fc6-706b-433f-9bcf-4ea4aaa09281', 'ton', - 'Ton', + 'Gram', Networks.main.ton, 9, UnderlyingAsset.TON, @@ -1429,7 +1429,7 @@ export const allCoinsAndTokens = [ account( '8244f85f-943c-4520-8e68-9e7f4361a13f', 'tton', - 'Testnet Ton', + 'Testnet Gram', Networks.test.ton, 9, UnderlyingAsset.TON, diff --git a/modules/statics/src/coins/ofcCoins.ts b/modules/statics/src/coins/ofcCoins.ts index 0493d2edac..526f68c358 100644 --- a/modules/statics/src/coins/ofcCoins.ts +++ b/modules/statics/src/coins/ofcCoins.ts @@ -203,7 +203,7 @@ export const ofcCoins = [ UnderlyingAsset.OPETH, CoinKind.CRYPTO ), - ofc('07083ea6-74ba-4da7-8cf3-031126a130a4', 'ofcton', 'Ton', 9, UnderlyingAsset.TON, CoinKind.CRYPTO), + ofc('07083ea6-74ba-4da7-8cf3-031126a130a4', 'ofcton', 'Gram', 9, UnderlyingAsset.TON, CoinKind.CRYPTO), ofc( '055691ec-f750-4349-b505-92954ca08257', 'ofccoredao', @@ -990,7 +990,7 @@ export const ofcCoins = [ UnderlyingAsset.OPETH, CoinKind.CRYPTO ), - tofc('b364799a-e6d1-4d84-afc9-588594e850f7', 'ofctton', 'Test Ton', 9, UnderlyingAsset.TON, CoinKind.CRYPTO), + tofc('b364799a-e6d1-4d84-afc9-588594e850f7', 'ofctton', 'Test Gram', 9, UnderlyingAsset.TON, CoinKind.CRYPTO), tofc('d7ec69dc-619d-4c10-b269-75c2327bd69d', 'ofcttao', 'Testnet Bittensor', 9, UnderlyingAsset.TAO, CoinKind.CRYPTO), ofc('8d329a6a-7b7a-4663-b3a4-a027fc3386d8', 'ofciota', 'Iota', 9, UnderlyingAsset.IOTA, CoinKind.CRYPTO), tofc('35d5b5eb-f61d-428d-8908-2f161507511f', 'ofctiota', 'Testnet Iota', 9, UnderlyingAsset.IOTA, CoinKind.CRYPTO), diff --git a/modules/statics/src/networks.ts b/modules/statics/src/networks.ts index fbed8a2a60..9d22d96d6e 100644 --- a/modules/statics/src/networks.ts +++ b/modules/statics/src/networks.ts @@ -1137,13 +1137,13 @@ class DydxTestnet extends Testnet implements CosmosNetwork { } class Ton extends Mainnet implements AccountNetwork { - name = 'Ton'; + name = 'Gram'; family = CoinFamily.TON; explorerUrl = 'https://tonscan.org/tx/'; } class TonTestnet extends Testnet implements AccountNetwork { - name = 'Testnet Ton'; + name = 'Testnet Gram'; family = CoinFamily.TON; explorerUrl = 'https://testnet.tonscan.org/tx/'; }