From 1af721e54a27697542d60755d81e3fc55d48affc Mon Sep 17 00:00:00 2001 From: ArunBala-Bitgo Date: Fri, 9 Jan 2026 16:01:53 +0530 Subject: [PATCH 1/4] test(statics): add test coverage for token config methods - Export getFormattedTokensByNetwork and verifyTokens functions - Add tests for duplicate token type detection - Add tests for contract address lowercase validation - Add tests for network filtering (Mainnet/Testnet) Ticket: WIN-8584 --- modules/statics/src/tokenConfig.ts | 4 +- modules/statics/test/unit/tokenConfigTests.ts | 209 ++++++++++++++++++ 2 files changed, 211 insertions(+), 2 deletions(-) diff --git a/modules/statics/src/tokenConfig.ts b/modules/statics/src/tokenConfig.ts index 60c7b76128..353b8943e9 100644 --- a/modules/statics/src/tokenConfig.ts +++ b/modules/statics/src/tokenConfig.ts @@ -1191,7 +1191,7 @@ const mergeEthLikeTokenMap = (...maps: EthLikeTokenMap[]): EthLikeTokenMap => { return mergedMap; }; -const getFormattedTokensByNetwork = (network: 'Mainnet' | 'Testnet', coinMap: typeof coins) => { +export const getFormattedTokensByNetwork = (network: 'Mainnet' | 'Testnet', coinMap: typeof coins) => { const networkType = network === 'Mainnet' ? NetworkType.MAINNET : NetworkType.TESTNET; const ethLikeTokenMap = getEthLikeTokens(network, TokenTypeEnum.ERC20); @@ -1357,7 +1357,7 @@ export const getFormattedTokens = (coinMap = coins): Tokens => { * Verify mainnet or testnet tokens * @param tokens */ -const verifyTokens = function (tokens: BaseTokenConfig[]) { +export const verifyTokens = function (tokens: BaseTokenConfig[]) { const verifiedTokens: Record = {}; tokens.forEach((token) => { if (verifiedTokens[token.type]) { diff --git a/modules/statics/test/unit/tokenConfigTests.ts b/modules/statics/test/unit/tokenConfigTests.ts index b7f4adc649..590629bb34 100644 --- a/modules/statics/test/unit/tokenConfigTests.ts +++ b/modules/statics/test/unit/tokenConfigTests.ts @@ -14,8 +14,12 @@ import { getFormattedEthLikeTokenConfig, getEthLikeTokens, getFormattedTokens, + getFormattedTokensByNetwork, + verifyTokens, EthLikeTokenConfig, TokenTypeEnum, + BaseTokenConfig, + BaseContractAddressConfig, } from '../../src/tokenConfig'; import { EthLikeERC20Token } from '../../src/account'; @@ -552,4 +556,209 @@ describe('EthLike Token Config Functions', function () { }); }); }); + + describe('getFormattedTokensByNetwork', function () { + it('should return tokens for Mainnet network', function () { + const result = getFormattedTokensByNetwork('Mainnet', coins); + + result.should.be.an.Object(); + result.should.have.property('eth'); + result.eth.should.have.property('tokens'); + result.eth.should.have.property('nfts'); + + // All eth tokens should be Mainnet + result.eth.tokens.forEach((token) => { + token.network.should.equal('Mainnet'); + }); + }); + + it('should return tokens for Testnet network', function () { + const result = getFormattedTokensByNetwork('Testnet', coins); + + result.should.be.an.Object(); + result.should.have.property('eth'); + result.eth.should.have.property('tokens'); + result.eth.should.have.property('nfts'); + + // All eth tokens should be Testnet + result.eth.tokens.forEach((token) => { + token.network.should.equal('Testnet'); + }); + }); + + it('should return the same chain keys for both networks', function () { + const mainnetResult = getFormattedTokensByNetwork('Mainnet', coins); + const testnetResult = getFormattedTokensByNetwork('Testnet', coins); + + const mainnetKeys = Object.keys(mainnetResult).sort(); + const testnetKeys = Object.keys(testnetResult).sort(); + + mainnetKeys.should.deepEqual(testnetKeys); + }); + + it('should have no duplicate token types within any chain', function () { + const mainnetResult = getFormattedTokensByNetwork('Mainnet', coins); + const testnetResult = getFormattedTokensByNetwork('Testnet', coins); + + // Check for duplicates in Mainnet + Object.entries(mainnetResult).forEach(([chain, chainData]) => { + if (chainData.tokens && chainData.tokens.length > 0) { + const tokenTypes = chainData.tokens.map((t) => t.type); + const uniqueTokenTypes = new Set(tokenTypes); + const duplicates = tokenTypes.filter((t, i) => tokenTypes.indexOf(t) !== i); + tokenTypes.length.should.equal( + uniqueTokenTypes.size, + `Mainnet ${chain} has duplicate token types: ${duplicates}` + ); + } + }); + + // Check for duplicates in Testnet + Object.entries(testnetResult).forEach(([chain, chainData]) => { + if (chainData.tokens && chainData.tokens.length > 0) { + const tokenTypes = chainData.tokens.map((t) => t.type); + const uniqueTokenTypes = new Set(tokenTypes); + const duplicates = tokenTypes.filter((t, i) => tokenTypes.indexOf(t) !== i); + tokenTypes.length.should.equal( + uniqueTokenTypes.size, + `Testnet ${chain} has duplicate token types: ${duplicates}` + ); + } + }); + }); + + it('should filter tokens correctly by network type', function () { + const mainnetResult = getFormattedTokensByNetwork('Mainnet', coins); + const testnetResult = getFormattedTokensByNetwork('Testnet', coins); + + // Verify no testnet tokens in mainnet result + Object.values(mainnetResult).forEach((chainData) => { + if (chainData.tokens && chainData.tokens.length > 0) { + chainData.tokens.forEach((token) => { + if (token && token.network) { + token.network.should.equal('Mainnet'); + } + }); + } + if ('nfts' in chainData && chainData.nfts && chainData.nfts.length > 0) { + chainData.nfts.forEach((nft) => { + if (nft && nft.network) { + nft.network.should.equal('Mainnet'); + } + }); + } + }); + + // Verify no mainnet tokens in testnet result + Object.values(testnetResult).forEach((chainData) => { + if (chainData.tokens && chainData.tokens.length > 0) { + chainData.tokens.forEach((token) => { + if (token && token.network) { + token.network.should.equal('Testnet'); + } + }); + } + if ('nfts' in chainData && chainData.nfts && chainData.nfts.length > 0) { + chainData.nfts.forEach((nft) => { + if (nft && nft.network) { + nft.network.should.equal('Testnet'); + } + }); + } + }); + }); + }); + + describe('verifyTokens', function () { + it('should return verified tokens record when no duplicates exist', function () { + const mockTokens: BaseTokenConfig[] = [ + { type: 'token1', coin: 'eth', name: 'Token 1', decimalPlaces: 18 }, + { type: 'token2', coin: 'eth', name: 'Token 2', decimalPlaces: 18 }, + { type: 'token3', coin: 'eth', name: 'Token 3', decimalPlaces: 6 }, + ]; + + const result = verifyTokens(mockTokens); + + result.should.be.an.Object(); + result.should.have.property('token1', true); + result.should.have.property('token2', true); + result.should.have.property('token3', true); + }); + + it('should throw an error when duplicate token types exist', function () { + const mockTokensWithDuplicates: BaseTokenConfig[] = [ + { type: 'token1', coin: 'eth', name: 'Token 1', decimalPlaces: 18 }, + { type: 'token2', coin: 'eth', name: 'Token 2', decimalPlaces: 18 }, + { type: 'token1', coin: 'eth', name: 'Token 1 Duplicate', decimalPlaces: 18 }, // Duplicate + ]; + + (() => { + verifyTokens(mockTokensWithDuplicates); + }).should.throw('token : token1 duplicated.'); + }); + + it('should throw an error when token contract address is not lowercase', function () { + const mockTokensWithUppercaseAddress: BaseContractAddressConfig[] = [ + { + type: 'token1', + coin: 'eth', + name: 'Token 1', + decimalPlaces: 18, + network: 'Mainnet', + tokenContractAddress: '0xAbCdEf1234567890AbCdEf1234567890AbCdEf12', // Mixed case + }, + ]; + + (() => { + verifyTokens(mockTokensWithUppercaseAddress); + }).should.throw(/token contract: token1 is not all lower case/); + }); + + it('should pass when token contract address is lowercase', function () { + const mockTokensWithLowercaseAddress: BaseContractAddressConfig[] = [ + { + type: 'token1', + coin: 'eth', + name: 'Token 1', + decimalPlaces: 18, + network: 'Mainnet', + tokenContractAddress: '0xabcdef1234567890abcdef1234567890abcdef12', // All lowercase + }, + ]; + + const result = verifyTokens(mockTokensWithLowercaseAddress); + result.should.have.property('token1', true); + }); + + it('should handle empty token array', function () { + const result = verifyTokens([]); + + result.should.be.an.Object(); + Object.keys(result).length.should.equal(0); + }); + + it('should verify real tokens from getFormattedTokens have no duplicates', function () { + const formattedTokens = getFormattedTokens(); + + // Test mainnet eth tokens + (() => { + verifyTokens(formattedTokens.bitcoin.eth.tokens); + }).should.not.throw(); + + // Test testnet eth tokens + (() => { + verifyTokens(formattedTokens.testnet.eth.tokens); + }).should.not.throw(); + + // Test mainnet xlm tokens + (() => { + verifyTokens(formattedTokens.bitcoin.xlm.tokens); + }).should.not.throw(); + + // Test testnet xlm tokens + (() => { + verifyTokens(formattedTokens.testnet.xlm.tokens); + }).should.not.throw(); + }); + }); }); From 9fa7feeae89b10395a2392d5a07b04cdca132f22 Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Tue, 13 Jan 2026 17:02:42 +0100 Subject: [PATCH 2/4] feat(deps): bump version to 1.24.0 Update @bitgo/wasm-utxo dependency across modules to use the latest version 1.24.0. Issue: BTC-0 Co-authored-by: llm-git --- modules/abstract-utxo/package.json | 2 +- modules/utxo-bin/package.json | 2 +- modules/utxo-core/package.json | 2 +- modules/utxo-staking/package.json | 2 +- yarn.lock | 24 ++++++++++++------------ 5 files changed, 16 insertions(+), 16 deletions(-) diff --git a/modules/abstract-utxo/package.json b/modules/abstract-utxo/package.json index 9411ca8a89..97004b433a 100644 --- a/modules/abstract-utxo/package.json +++ b/modules/abstract-utxo/package.json @@ -68,7 +68,7 @@ "@bitgo/utxo-core": "^1.28.0", "@bitgo/utxo-lib": "^11.19.0", "@bitgo/utxo-ord": "^1.22.20", - "@bitgo/wasm-utxo": "^1.22.0", + "@bitgo/wasm-utxo": "^1.24.0", "@types/lodash": "^4.14.121", "@types/superagent": "4.1.15", "bignumber.js": "^9.0.2", diff --git a/modules/utxo-bin/package.json b/modules/utxo-bin/package.json index 478ed69c3e..bce00e80e0 100644 --- a/modules/utxo-bin/package.json +++ b/modules/utxo-bin/package.json @@ -31,7 +31,7 @@ "@bitgo/unspents": "^0.50.13", "@bitgo/utxo-core": "^1.28.0", "@bitgo/utxo-lib": "^11.19.0", - "@bitgo/wasm-utxo": "^1.22.0", + "@bitgo/wasm-utxo": "^1.24.0", "@noble/curves": "1.8.1", "archy": "^1.0.0", "bech32": "^2.0.0", diff --git a/modules/utxo-core/package.json b/modules/utxo-core/package.json index 4092e6e5fd..dbf00d3e7e 100644 --- a/modules/utxo-core/package.json +++ b/modules/utxo-core/package.json @@ -81,7 +81,7 @@ "@bitgo/secp256k1": "^1.8.0", "@bitgo/unspents": "^0.50.13", "@bitgo/utxo-lib": "^11.19.0", - "@bitgo/wasm-utxo": "^1.22.0", + "@bitgo/wasm-utxo": "^1.24.0", "bip174": "npm:@bitgo-forks/bip174@3.1.0-master.4", "fast-sha256": "^1.3.0" }, diff --git a/modules/utxo-staking/package.json b/modules/utxo-staking/package.json index 12160669fe..01288089fe 100644 --- a/modules/utxo-staking/package.json +++ b/modules/utxo-staking/package.json @@ -63,7 +63,7 @@ "@bitgo/babylonlabs-io-btc-staking-ts": "^3.3.0", "@bitgo/utxo-core": "^1.28.0", "@bitgo/utxo-lib": "^11.19.0", - "@bitgo/wasm-utxo": "^1.22.0", + "@bitgo/wasm-utxo": "^1.24.0", "bip174": "npm:@bitgo-forks/bip174@3.1.0-master.4", "bip322-js": "^2.0.0", "bitcoinjs-lib": "^6.1.7", diff --git a/yarn.lock b/yarn.lock index 0718ce1161..544fc739a3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -974,10 +974,10 @@ "@scure/base" "1.1.5" micro-eth-signer "0.7.2" -"@bitgo/public-types@5.63.0": - version "5.63.0" - resolved "https://registry.npmjs.org/@bitgo/public-types/-/public-types-5.63.0.tgz#1f75a376bcd9e340106e2607ff5508280b66f152" - integrity sha512-9UjiUbX1m2HBvFI2mQ9CqOfJl0bujMPUtAf8Lf14vQ5f/IvM7sfPcve0fwf2yFAFmzfLbBxazQ1ZW4g5GtRN6A== +"@bitgo/public-types@5.61.0": + version "5.61.0" + resolved "https://registry.npmjs.org/@bitgo/public-types/-/public-types-5.61.0.tgz#38b4c6f0258a6700683daf698226ed20a22da944" + integrity sha512-IP7NJDhft0Vt+XhrAHOtUAroUfe2yy4i1I4oZgZXwjbYkLIKqKWarQDs/V/toh6vHdRTxtTuqI27TPcnI2IuTw== dependencies: fp-ts "^2.0.0" io-ts "npm:@bitgo-forks/io-ts@2.1.4" @@ -985,10 +985,10 @@ monocle-ts "^2.3.13" newtype-ts "^0.3.5" -"@bitgo/public-types@5.61.0": - version "5.61.0" - resolved "https://registry.npmjs.org/@bitgo/public-types/-/public-types-5.61.0.tgz#38b4c6f0258a6700683daf698226ed20a22da944" - integrity sha512-IP7NJDhft0Vt+XhrAHOtUAroUfe2yy4i1I4oZgZXwjbYkLIKqKWarQDs/V/toh6vHdRTxtTuqI27TPcnI2IuTw== +"@bitgo/public-types@5.63.0": + version "5.63.0" + resolved "https://registry.npmjs.org/@bitgo/public-types/-/public-types-5.63.0.tgz#1f75a376bcd9e340106e2607ff5508280b66f152" + integrity sha512-9UjiUbX1m2HBvFI2mQ9CqOfJl0bujMPUtAf8Lf14vQ5f/IvM7sfPcve0fwf2yFAFmzfLbBxazQ1ZW4g5GtRN6A== dependencies: fp-ts "^2.0.0" io-ts "npm:@bitgo-forks/io-ts@2.1.4" @@ -996,10 +996,10 @@ monocle-ts "^2.3.13" newtype-ts "^0.3.5" -"@bitgo/wasm-utxo@^1.22.0": - version "1.22.0" - resolved "https://registry.npmjs.org/@bitgo/wasm-utxo/-/wasm-utxo-1.22.0.tgz#106cb3ddcdaf39753a513aca5c8e0508faba5dc7" - integrity sha512-/2jPyJvb3OwoFJ4fYI8V28zQVwj5ma6y17mByDFtMz7td0SraycPqYP6Y0B+YcVlqTMlZ0SYoEGKXBqeBqPy6w== +"@bitgo/wasm-utxo@^1.24.0": + version "1.24.0" + resolved "https://registry.npmjs.org/@bitgo/wasm-utxo/-/wasm-utxo-1.24.0.tgz#27c28b496daad594fa0b20d7ced654dbeeb3473f" + integrity sha512-7AEBQJ03V8JWiH1SEkrf6j4IAjo6Tl/G7QHtmBXwoMs5Bpy0haZMERl0eodmiCIczHYGTmpk6fgGNyvaVflg7A== "@brandonblack/musig@^0.0.1-alpha.0": version "0.0.1-alpha.1" From 1754f4909dab3bad3591cb1176b8be6f260ffdb9 Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Wed, 14 Jan 2026 14:24:35 +0100 Subject: [PATCH 3/4] fix(utxo-ord): fix inscription reveal size test Update the test to match actual reveal transaction size by using the same output address in both estimation and the actual transaction. Also increase commit value for a more realistic test case. Issue: BTC-2936 Co-authored-by: llm-git --- modules/utxo-ord/test/inscription.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/modules/utxo-ord/test/inscription.ts b/modules/utxo-ord/test/inscription.ts index 0ec771bf41..f6ab938449 100644 --- a/modules/utxo-ord/test/inscription.ts +++ b/modules/utxo-ord/test/inscription.ts @@ -9,7 +9,7 @@ function createCommitTransactionPsbt(commitAddress: string, walletKeys: utxolib. commitTransactionPsbt.addOutput({ script: commitTransactionOutputScript, - value: BigInt(42), + value: BigInt(10_000), }); const walletUnspent = testutil.mockWalletUnspent(networks.testnet, BigInt(20_000), { keys: walletKeys }); @@ -64,7 +64,7 @@ describe('inscriptions', () => { }); }); - xdescribe('Inscription Reveal Data', () => { + describe('Inscription Reveal Data', () => { it('should sign reveal transaction and validate reveal size', () => { const walletKeys = testutil.getDefaultWalletKeys(); const inscriptionData = Buffer.from('And Desert You', 'ascii'); @@ -76,11 +76,13 @@ describe('inscriptions', () => { ); const commitTransactionPsbt = createCommitTransactionPsbt(address, walletKeys); + // Use the commit address (P2TR) as recipient to match the output script size + // used in getInscriptionRevealSize estimation const fullySignedRevealTransaction = inscriptions.signRevealTransaction( walletKeys.user.privateKey as Buffer, tapLeafScript, address, - '2N9R3mMCv6UfVbWEUW3eXJgxDeg4SCUVsu9', + address, commitTransactionPsbt.getUnsignedTx().toBuffer(), networks.testnet ); @@ -88,7 +90,6 @@ describe('inscriptions', () => { fullySignedRevealTransaction.finalizeTapInputWithSingleLeafScriptAndSignature(0); const actualVirtualSize = fullySignedRevealTransaction.extractTransaction(true).virtualSize(); - // TODO(BG-70861): figure out why size is slightly different and re-enable test assert.strictEqual(revealTransactionVSize, actualVirtualSize); }); }); From 4d4bae3982657ab81b533547719d36776dffc4d3 Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Wed, 14 Jan 2026 14:34:50 +0100 Subject: [PATCH 4/4] feat(utxo-ord): use constant for default postage amount Define a default postage amount constant based on the ord reference implementation. Replace the hardcoded value with this named constant for better readability and maintainability. Issue: BTC-2936 Co-authored-by: llm-git --- modules/utxo-ord/src/inscriptions.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/modules/utxo-ord/src/inscriptions.ts b/modules/utxo-ord/src/inscriptions.ts index 2d5f0e5879..7e8f92c31f 100644 --- a/modules/utxo-ord/src/inscriptions.ts +++ b/modules/utxo-ord/src/inscriptions.ts @@ -21,6 +21,9 @@ import { PreparedInscriptionRevealData } from '@bitgo/sdk-core'; const OPS = bscript.OPS; const MAX_LENGTH_TAP_DATA_PUSH = 520; +// default "postage" amount +// https://github.com/ordinals/ord/blob/0.24.2/src/lib.rs#L149 +const DEFAULT_POSTAGE_AMOUNT = BigInt(10_000); /** * The max size of an individual OP_PUSH in a Taproot script is 520 bytes. This @@ -100,7 +103,7 @@ function getInscriptionRevealSize( }, ], }); - psbt.addOutput({ script: commitOutput, value: BigInt(10_000) }); + psbt.addOutput({ script: commitOutput, value: DEFAULT_POSTAGE_AMOUNT }); psbt.signTaprootInput( 0,