diff --git a/modules/abstract-utxo/package.json b/modules/abstract-utxo/package.json index 55611be5c3..5b299fa6c4 100644 --- a/modules/abstract-utxo/package.json +++ b/modules/abstract-utxo/package.json @@ -68,7 +68,7 @@ "@bitgo/utxo-core": "^1.30.0", "@bitgo/utxo-lib": "^11.19.1", "@bitgo/utxo-ord": "^1.22.22", - "@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/statics/src/tokenConfig.ts b/modules/statics/src/tokenConfig.ts index 0797990c72..608dbe52fc 100644 --- a/modules/statics/src/tokenConfig.ts +++ b/modules/statics/src/tokenConfig.ts @@ -1192,7 +1192,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); @@ -1358,7 +1358,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(); + }); + }); }); diff --git a/modules/utxo-bin/package.json b/modules/utxo-bin/package.json index 9562f4c9f6..1af9ee4a00 100644 --- a/modules/utxo-bin/package.json +++ b/modules/utxo-bin/package.json @@ -31,7 +31,7 @@ "@bitgo/unspents": "^0.50.14", "@bitgo/utxo-core": "^1.30.0", "@bitgo/utxo-lib": "^11.19.1", - "@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 30578bacd3..f234b1bb75 100644 --- a/modules/utxo-core/package.json +++ b/modules/utxo-core/package.json @@ -81,7 +81,7 @@ "@bitgo/secp256k1": "^1.9.0", "@bitgo/unspents": "^0.50.14", "@bitgo/utxo-lib": "^11.19.1", - "@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-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, 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); }); }); diff --git a/modules/utxo-staking/package.json b/modules/utxo-staking/package.json index 9ac411c8f0..dece000de3 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.30.0", "@bitgo/utxo-lib": "^11.19.1", - "@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 7526454e8b..544fc739a3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"