diff --git a/modules/sdk-core/package.json b/modules/sdk-core/package.json index 12980516ee..3f22c84106 100644 --- a/modules/sdk-core/package.json +++ b/modules/sdk-core/package.json @@ -52,6 +52,7 @@ "big.js": "^3.1.3", "bigint-crypto-utils": "3.1.4", "bignumber.js": "^9.1.1", + "bolt11": "^1.0.0", "bs58": "^4.0.1", "create-hmac": "^1.1.7", "debug": "^3.1.0", diff --git a/modules/sdk-core/src/coins/ofcToken.ts b/modules/sdk-core/src/coins/ofcToken.ts index 84ffbb4406..1ee8fa844d 100644 --- a/modules/sdk-core/src/coins/ofcToken.ts +++ b/modules/sdk-core/src/coins/ofcToken.ts @@ -2,12 +2,14 @@ * @prettier */ import { OfcTokenConfig } from '@bitgo/statics'; +import * as bolt11 from 'bolt11'; import { isString } from 'lodash'; import { BitGoBase, CoinConstructor, SignTransactionOptions as BaseSignTransactionOptions, SignedTransaction, + ITransactionRecipient, } from '../'; import { Ofc } from './ofc'; @@ -21,6 +23,18 @@ export interface SignTransactionOptions extends BaseSignTransactionOptions { export { OfcTokenConfig }; const publicIdRegex = /^[a-f\d]{32}$/i; + +function isBolt11Invoice(value: unknown): value is string { + if (!isString(value)) { + return false; + } + try { + bolt11.decode(value); + return true; + } catch (_e) { + return false; + } +} export class OfcToken extends Ofc { public readonly tokenConfig: OfcTokenConfig; @@ -65,6 +79,30 @@ export class OfcToken extends Ofc { return this.tokenConfig.type; } + checkRecipient(recipient: ITransactionRecipient): void { + if (isBolt11Invoice(recipient.address)) { + // amount for bolt11 invoices is either 'invoice' or a non-zero bigint + if (recipient.amount === 'invoice') { + return; + } + // try to parse the amount as a bigint + let amount: bigint; + try { + amount = BigInt(recipient.amount); + } catch (e) { + throw new Error( + `invalid argument ${recipient.amount} for amount - lightning invoice amount must be >= 0 or 'invoice'` + ); + } + if (amount > 0n) { + return; + } + throw new Error(`invalid argument for amount - lightning invoice amount must be a non-zero bigint or 'invoice'`); + } + + super.checkRecipient(recipient); + } + /** * Flag for sending value of 0 * @returns {boolean} True if okay to send 0 value, false otherwise diff --git a/modules/sdk-core/test/unit/coins/ofcToken.ts b/modules/sdk-core/test/unit/coins/ofcToken.ts new file mode 100644 index 0000000000..0fa6a772bc --- /dev/null +++ b/modules/sdk-core/test/unit/coins/ofcToken.ts @@ -0,0 +1,41 @@ +import 'should'; + +import { OfcToken } from '../../../src/coins/ofcToken'; + +describe('OfcToken.checkRecipient', () => { + const token = new OfcToken( + {} as any, + { + coin: 'tofc', + decimalPlaces: 2, + name: 'Test OFC Token', + backingCoin: 'ofc', + isFiat: false, + type: 'tofc', + } as any + ); + + // Valid BOLT11 invoice (taken from BitGoJS fixtures / BOLTs examples) + const bolt11 = + 'lntb20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygshp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqfpp3x9et2e20v6pu37c5d9vax37wxq72un989qrsgqdj545axuxtnfemtpwkc45hx9d2ft7x04mt8q7y6t0k2dge9e7h8kpy9p34ytyslj3yu569aalz2xdk8xkd7ltxqld94u8h2esmsmacgpghe9k8'; + + it('should allow bolt11 invoices with amount "invoice"', () => { + (() => token.checkRecipient({ address: bolt11, amount: 'invoice' } as any)).should.not.throw(); + }); + + it('should allow bolt11 invoices with non-zero bigint amount', () => { + (() => token.checkRecipient({ address: bolt11, amount: 1n } as any)).should.not.throw(); + }); + + it('should reject bolt11 invoices with non-bigint numeric/string amounts', () => { + (() => token.checkRecipient({ address: bolt11, amount: '1' } as any)).should.throw(); + (() => token.checkRecipient({ address: bolt11, amount: 1 } as any)).should.throw(); + }); + + it('should defer to super.checkRecipient for non-bolt11 addresses', () => { + // BaseCoin.checkRecipient rejects zero amounts when valuelessTransferAllowed() is false (default for OfcToken). + (() => token.checkRecipient({ address: 'bg-0123456789abcdef0123456789abcdef', amount: '0' } as any)).should.throw(); + (() => + token.checkRecipient({ address: 'bg-0123456789abcdef0123456789abcdef', amount: '1' } as any)).should.not.throw(); + }); +}); diff --git a/yarn.lock b/yarn.lock index 544fc739a3..9ffea0b00b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7867,7 +7867,7 @@ bech32-buffer@^0.2.1: resolved "https://registry.npmjs.org/bech32-buffer/-/bech32-buffer-0.2.1.tgz" integrity sha512-fCG1TyZuCN48Sdw97p/IR39fvqpFlWDVpG7qnuU1Uc3+Xtc/0uqAp8U7bMW/bGuVF5CcNVIXwxQsWwUr6un6FQ== -bech32@1.1.4, bech32@^1.1.3, bech32@^1.1.4: +bech32@1.1.4, bech32@^1.1.2, bech32@^1.1.3, bech32@^1.1.4: version "1.1.4" resolved "https://registry.npmjs.org/bech32/-/bech32-1.1.4.tgz" integrity sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ== @@ -8014,7 +8014,7 @@ bitcoin-ops@^1.3.0: resolved "https://registry.npmjs.org/bitcoin-ops/-/bitcoin-ops-1.4.1.tgz" integrity sha512-pef6gxZFztEhaE9RY9HmWVmiIHqCb2OyS4HPKkpc6CIiiOa3Qmuoylxc5P2EkU3w+5eTSifI9SEZC88idAIGow== -bitcoinjs-lib@^6.1.5, bitcoinjs-lib@^6.1.7: +bitcoinjs-lib@^6.0.0, bitcoinjs-lib@^6.1.5, bitcoinjs-lib@^6.1.7: version "6.1.7" resolved "https://registry.npmjs.org/bitcoinjs-lib/-/bitcoinjs-lib-6.1.7.tgz" integrity sha512-tlf/r2DGMbF7ky1MgUqXHzypYHakkEnm0SZP23CJKIqNY/5uNAnMbFhMJdhjrL/7anfb/U8+AlpdjPWjPnAalg== @@ -8143,6 +8143,20 @@ body-parser@1.20.3, body-parser@^1.19.0, body-parser@^1.20.3: type-is "~1.6.18" unpipe "1.0.0" +bolt11@^1.0.0: + version "1.4.1" + resolved "https://registry.npmjs.org/bolt11/-/bolt11-1.4.1.tgz#4363041b8c9f477b7f42c12d96e771fec39a00f1" + integrity sha512-jR0Y+MO+CK2at1Cg5mltLJ+6tdOwNKoTS/DJOBDdzVkQ+R9D6UgZMayTWOsuzY7OgV1gEqlyT5Tzk6t6r4XcNQ== + dependencies: + "@types/bn.js" "^4.11.3" + bech32 "^1.1.2" + bitcoinjs-lib "^6.0.0" + bn.js "^4.11.8" + create-hash "^1.2.0" + lodash "^4.17.11" + safe-buffer "^5.1.1" + secp256k1 "^4.0.2" + bonjour-service@^1.2.1: version "1.3.0" resolved "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.3.0.tgz" @@ -12791,18 +12805,7 @@ html-minifier-terser@^6.0.2: tapable "^1.1.3" util.promisify "1.0.0" -"html-webpack-plugin-5@npm:html-webpack-plugin@^5": - version "5.6.4" - resolved "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.6.4.tgz" - integrity sha512-V/PZeWsqhfpE27nKeX9EO2sbR+D17A+tLf6qU+ht66jdUsN0QLKJN27Z+1+gHrVMKgndBahes0PU6rRihDgHTw== - dependencies: - "@types/html-minifier-terser" "^6.0.0" - html-minifier-terser "^6.0.2" - lodash "^4.17.21" - pretty-error "^4.0.0" - tapable "^2.0.0" - -html-webpack-plugin@^5.5.0: +"html-webpack-plugin-5@npm:html-webpack-plugin@^5", html-webpack-plugin@^5.5.0: version "5.6.4" resolved "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.6.4.tgz" integrity sha512-V/PZeWsqhfpE27nKeX9EO2sbR+D17A+tLf6qU+ht66jdUsN0QLKJN27Z+1+gHrVMKgndBahes0PU6rRihDgHTw== @@ -18689,7 +18692,7 @@ scrypt-js@3.0.1, scrypt-js@^3.0.0: resolved "https://registry.npmjs.org/scrypt-js/-/scrypt-js-3.0.1.tgz" integrity sha512-cdwTTnqPu0Hyvf5in5asVdZocVDTNRmR7XEcJuIzMjJeSHybHl7vpB66AzwTaIg6CLSbtjcxc8fqcySfnTkccA== -secp256k1@3.7.1, secp256k1@5.0.1, secp256k1@^3.0.1, secp256k1@^4.0.0, secp256k1@^4.0.1, secp256k1@^5.0.0: +secp256k1@3.7.1, secp256k1@5.0.1, secp256k1@^3.0.1, secp256k1@^4.0.0, secp256k1@^4.0.1, secp256k1@^4.0.2, secp256k1@^5.0.0: version "5.0.1" resolved "https://registry.npmjs.org/secp256k1/-/secp256k1-5.0.1.tgz" integrity sha512-lDFs9AAIaWP9UCdtWrotXWWF9t8PWgQDcxqgAnpM9rMqxb3Oaq2J0thzPVSxBwdJgyQtkU/sYtFtbM1RSt/iYA== @@ -19600,16 +19603,7 @@ string-argv@^0.3.1: resolved "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz" integrity sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q== -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -19685,7 +19679,7 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -19699,13 +19693,6 @@ strip-ansi@^3.0.1: dependencies: ansi-regex "^2.0.0" -strip-ansi@^6.0.0, strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - strip-ansi@^7.0.1: version "7.1.0" resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz" @@ -21428,7 +21415,7 @@ workerpool@^6.5.1: resolved "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz" integrity sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -21446,15 +21433,6 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz"