From d88365b2b50634df91040da39153b7228587a638 Mon Sep 17 00:00:00 2001 From: ronaldsg Date: Thu, 25 Apr 2024 01:37:23 -0500 Subject: [PATCH 1/5] Add xverse wallet support for pegin I've added the required classes in order to handle the connection with Xverse wallet in the pegin process --- package-lock.json | 128 +++++++++++++++- package.json | 1 + src/assets/wallet-icons/xverse-white.png | Bin 0 -> 963 bytes src/assets/wallet-icons/xverse.png | Bin 0 -> 944 bytes .../exchange/SelectBitcoinWallet.vue | 3 + src/common/services/XverseService.ts | 140 ++++++++++++++++++ src/common/services/index.ts | 1 + src/common/store/constants.ts | 1 + src/common/types/Common.ts | 1 + src/common/types/pegInTx.ts | 2 +- src/common/utils/btcAddressUtils.ts | 14 ++ src/common/walletConf.json | 10 ++ src/pegin/components/create/SendBitcoin.vue | 5 + .../middleware/TxBuilder/XverseTxBuilder.ts | 83 +++++++++++ src/pegin/store/actions.ts | 4 + src/pegin/store/getters.ts | 9 ++ src/sats-connect.d.ts | 103 +++++++++++++ 17 files changed, 497 insertions(+), 8 deletions(-) create mode 100644 src/assets/wallet-icons/xverse-white.png create mode 100644 src/assets/wallet-icons/xverse.png create mode 100644 src/common/services/XverseService.ts create mode 100644 src/pegin/middleware/TxBuilder/XverseTxBuilder.ts create mode 100644 src/sats-connect.d.ts diff --git a/package-lock.json b/package-lock.json index 69e6bd48..51617492 100644 --- a/package-lock.json +++ b/package-lock.json @@ -43,6 +43,7 @@ "moment": "^2.29.4", "os-browserify": "^0.3.0", "process": "^0.11.10", + "sats-connect": "2.3.x", "stackjs": "^1.0.0", "stream-browserify": "^3.0.0", "stream-http": "^3.2.0", @@ -4027,6 +4028,38 @@ "version": "1.0.1", "license": "ISC" }, + "node_modules/@sats-connect/core": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/@sats-connect/core/-/core-0.0.8.tgz", + "integrity": "sha512-vb7drnd8lFfO4ahCzaVAFkX1eHF1J7jheJl2V/JuuJd5f1sy6nHeNzKMp1zmiuql8uNwe0Sx1WrK1I+4tUmDHg==", + "dependencies": { + "axios": "1.6.8", + "bitcoin-address-validation": "2.2.3", + "buffer": "6.0.3", + "jsontokens": "4.0.1", + "lodash.omit": "4.5.0" + } + }, + "node_modules/@sats-connect/core/node_modules/bech32": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/bech32/-/bech32-2.0.0.tgz", + "integrity": "sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg==" + }, + "node_modules/@sats-connect/core/node_modules/bitcoin-address-validation": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/bitcoin-address-validation/-/bitcoin-address-validation-2.2.3.tgz", + "integrity": "sha512-1uGCGl26Ye8JG5qcExtFLQfuib6qEZWNDo1ZlLlwp/z7ygUFby3IxolgEfgMGaC+LG9csbVASLcH8fRLv7DIOg==", + "dependencies": { + "base58-js": "^1.0.0", + "bech32": "^2.0.0", + "sha256-uint8array": "^0.10.3" + } + }, + "node_modules/@sats-connect/ui": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/@sats-connect/ui/-/ui-0.0.5.tgz", + "integrity": "sha512-skGV6NaD708/k40AFmyphP1WLAYYhCj2t6dZY6Hb7F6qQg9ROP7yFn0g3CE0gFpA5Mtr3of/jmAMKv2p8e+j0Q==" + }, "node_modules/@scure/base": { "version": "1.1.1", "funding": [ @@ -7220,11 +7253,11 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz", - "integrity": "sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==", + "version": "1.6.8", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.8.tgz", + "integrity": "sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==", "dependencies": { - "follow-redirects": "^1.15.0", + "follow-redirects": "^1.15.6", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } @@ -7513,6 +7546,14 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/base58-js": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/base58-js/-/base58-js-1.0.5.tgz", + "integrity": "sha512-LkkAPP8Zu+c0SVNRTRVDyMfKVORThX+rCViget00xdgLRrKkClCTz1T7cIrpr69ShwV5XJuuoZvMvJ43yURwkA==", + "engines": { + "node": ">= 8" + } + }, "node_modules/base64-js": { "version": "1.5.1", "funding": [ @@ -12489,9 +12530,9 @@ "license": "ISC" }, "node_modules/follow-redirects": { - "version": "1.15.4", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.4.tgz", - "integrity": "sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==", + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", "funding": [ { "type": "individual", @@ -16966,6 +17007,11 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.omit": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.omit/-/lodash.omit-4.5.0.tgz", + "integrity": "sha512-XeqSp49hNGmlkj2EJlfrQFIzQ6lXdNro9sddtQzcJY8QaoC2GO0DT7xaIokHeyM+mIT0mPMlPvkYzg2xCuHdZg==" + }, "node_modules/lodash.truncate": { "version": "4.4.2", "dev": true, @@ -20481,6 +20527,69 @@ } } }, + "node_modules/sats-connect": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/sats-connect/-/sats-connect-2.3.0.tgz", + "integrity": "sha512-4iRyXwyX60rnFs6EW+19MR27YEEOo1Hq2SfcRfN0Xugowme29fWi56oDEUGV33y8inWrKd+LPxnr4ynhPBxWKw==", + "dependencies": { + "@sats-connect/core": "0.0.8", + "@sats-connect/make-default-provider-config": "0.0.2", + "@sats-connect/ui": "0.0.5" + } + }, + "node_modules/sats-connect/node_modules/@sats-connect/make-default-provider-config": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@sats-connect/make-default-provider-config/-/make-default-provider-config-0.0.2.tgz", + "integrity": "sha512-kuJdHa0J6KYDMTkQN1Hy92t2trT+Xql76ZEDMJICfIZzflyQlErK75z87TYNtKJujV9uGzEYEYxUguxmcldNdQ==", + "dependencies": { + "@sats-connect/core": "0.0.7", + "@sats-connect/ui": "0.0.5", + "bowser": "2.11.0" + }, + "peerDependencies": { + "typescript": "5.4.4" + } + }, + "node_modules/sats-connect/node_modules/@sats-connect/make-default-provider-config/node_modules/@sats-connect/core": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/@sats-connect/core/-/core-0.0.7.tgz", + "integrity": "sha512-4m5amq+orHDbqLqCRWojvDQigKAys33Ntwc7U5xNtFeib4j+DpYz6lVAL/s3cay1kq03WUZ+Gil3l5rv+5bQWQ==", + "dependencies": { + "axios": "1.6.8", + "bitcoin-address-validation": "2.2.3", + "buffer": "6.0.3", + "jsontokens": "4.0.1", + "lodash.omit": "4.5.0" + } + }, + "node_modules/sats-connect/node_modules/bech32": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/bech32/-/bech32-2.0.0.tgz", + "integrity": "sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg==" + }, + "node_modules/sats-connect/node_modules/bitcoin-address-validation": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/bitcoin-address-validation/-/bitcoin-address-validation-2.2.3.tgz", + "integrity": "sha512-1uGCGl26Ye8JG5qcExtFLQfuib6qEZWNDo1ZlLlwp/z7ygUFby3IxolgEfgMGaC+LG9csbVASLcH8fRLv7DIOg==", + "dependencies": { + "base58-js": "^1.0.0", + "bech32": "^2.0.0", + "sha256-uint8array": "^0.10.3" + } + }, + "node_modules/sats-connect/node_modules/typescript": { + "version": "5.4.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.4.tgz", + "integrity": "sha512-dGE2Vv8cpVvw28v8HCPqyb08EzbBURxDpuhJvTrusShUfGnhHBafDsLdS1EhhxyL6BJQE+2cT3dDPAv+MQ6oLw==", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/saxes": { "version": "5.0.1", "license": "ISC", @@ -20793,6 +20902,11 @@ "sha.js": "bin.js" } }, + "node_modules/sha256-uint8array": { + "version": "0.10.7", + "resolved": "https://registry.npmjs.org/sha256-uint8array/-/sha256-uint8array-0.10.7.tgz", + "integrity": "sha512-1Q6JQU4tX9NqsDGodej6pkrUVQVNapLZnvkwIhddH/JqzBZF1fSaxSWNY6sziXBE8aEa2twtGkXUrwzGeZCMpQ==" + }, "node_modules/shallow-clone": { "version": "3.0.1", "dev": true, diff --git a/package.json b/package.json index b9f857f2..4d2c3bcc 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "moment": "^2.29.4", "os-browserify": "^0.3.0", "process": "^0.11.10", + "sats-connect": "2.3.x", "stackjs": "^1.0.0", "stream-browserify": "^3.0.0", "stream-http": "^3.2.0", diff --git a/src/assets/wallet-icons/xverse-white.png b/src/assets/wallet-icons/xverse-white.png new file mode 100644 index 0000000000000000000000000000000000000000..76b3e19ab399efeca2e84400dc9f883b7cab3884 GIT binary patch literal 963 zcmeAS@N?(olHy`uVBq!ia0vp^k|4~%1|*NXY)uAIEa{HEjtmSN`?>!lvI6-E$sR$z z3=CCj3=9n|3=F@3LJcn%7)lKo7+xhXFj&oCU=S~uvn$XBDAAG{;hE;^%b*2hb1<+n z3NbJPS&Tr)z$nE4G7ZRL@M4sPvx68lplX;H7}_%#SfFa6fHVkr05M1pgl1mAh%j*h z6I`|A0%imoq;2)dU$H=nv%n*=n1O*?7=#%aX3dcRniiQE5>XQ2>tmIipR1RclAn~S zSCLx)(#2p?VFhI7rj{fsROII56h?X&sHg;q@=(~U%$M(T(8_%FTW^V-_X+1Qs2Nx-^fT8 zs6w~6GOr}DLN~8i8Da>`9GBGM%@E9OzK% zs1tVhf2a9G@)50$D)ENntP{_7uy)*froCwT(tM%MYv)S5r> Nnx3wHF6*2UngC^jDYgIr literal 0 HcmV?d00001 diff --git a/src/assets/wallet-icons/xverse.png b/src/assets/wallet-icons/xverse.png new file mode 100644 index 0000000000000000000000000000000000000000..c65e0b6640571aa159986b58ed2dc35d2cb8e4ad GIT binary patch literal 944 zcmeAS@N?(olHy`uVBq!ia0vp^k|4~%1|*NXY)uAIEa{HEjtmSN`?>!lvI6-E$sR$z z3=CCj3=9n|3=F@3LJcn%7)lKo7+xhXFj&oCU=S~uvn$XBDAAG{;hE;^%b*2hb1<+n z3NbJPS&Tr)z$nE4G7ZRL@M4sPvx68lplX;H7}_%#SfFa6fHVkr05M1pgl1mAh%j*h z6I`|A0%imoq;2)dU$H=nv%n*=n1O*?7=#%aX3dcRniiQE5>XQ2>tmIipR1RclAn~S zSCLx)(#2p?VFhI7rj{fsROII56h?X&sHg;q@=(~U%$M(T(8_%FTW^V-_X+1Qs2Nx-^fT8 zs6w~6GOr}DLN~8i8Da>`9GBGMt<@S1PD>ORIVTITM=-E@{7_X$6q>;B zp7+#K{oGl7PxWGN?U|lm;_jtst7ZAvXM*y^_8I$b?Rllxs;t12$IFmzsu&o+f1&k@ zgP;S)qsBeji}cfjHlJ+Y`kZI3Lh+n{D(c)`fc$*FOAKBig|r^>#s=ElU*h34H;kzf2sB+Zc8%5IhjyRlB-A z+?*+ZL89wS{##pn1+jSs-_>5^XqfGMZks;KhOzq3dKt|%EsxebPv5WoNJ0Nx#;yf( spYwfFd!aY4jGN=R{|U7W?GElLCbz~U{&f?=Z-LUGr>mdKI;Vst02W6VQvd(} literal 0 HcmV?d00001 diff --git a/src/common/components/exchange/SelectBitcoinWallet.vue b/src/common/components/exchange/SelectBitcoinWallet.vue index 77acb323..e173c1ae 100644 --- a/src/common/components/exchange/SelectBitcoinWallet.vue +++ b/src/common/components/exchange/SelectBitcoinWallet.vue @@ -88,6 +88,9 @@ export default { case constants.WALLET_NAMES.LEATHER.long_name: wallet = constants.WALLET_NAMES.LEATHER.short_name; break; + case constants.WALLET_NAMES.XVERSE.long_name: + wallet = constants.WALLET_NAMES.XVERSE.short_name; + break; default: wallet = ''; break; diff --git a/src/common/services/XverseService.ts b/src/common/services/XverseService.ts new file mode 100644 index 00000000..ef2dff19 --- /dev/null +++ b/src/common/services/XverseService.ts @@ -0,0 +1,140 @@ +/* eslint-disable class-methods-use-this */ +import Wallet, { AddressPurpose, BitcoinNetworkType } from 'sats-connect'; +import * as bitcoin from 'bitcoinjs-lib'; +import { WalletService } from '@/common/services/index'; +import * as constants from '@/common/store/constants'; +import { XverseTx } from '@/pegin/middleware/TxBuilder/XverseTxBuilder'; +import { + WalletAddress, Tx, SignedTx, BtcAccount, Step, +} from '../types'; + +export default class XverseService extends WalletService { + satsBtcNetwork: BitcoinNetworkType; + + constructor() { + super(); + switch (this.network) { + case constants.BTC_NETWORK_MAINNET: + this.satsBtcNetwork = BitcoinNetworkType.Mainnet; + break; + default: + this.satsBtcNetwork = BitcoinNetworkType.Testnet; + break; + } + } + + getAccountAddresses(): Promise { + return new Promise((resolve, reject) => { + const walletAddresses: WalletAddress[] = []; + const payload = { + purposes: ['ordinals', 'payment'] as AddressPurpose[], + message: 'Welcome to the 2wp-app, please select your Bitcoin account to start.', + network: { + type: this.satsBtcNetwork, + }, + }; + Wallet.request('getAddresses', payload) + .then((response) => { + if (response.status === 'error') { + reject(new Error(response.error.message)); + } else { + response.result.addresses + .forEach((addr: { address: string; publicKey: string; }) => { + walletAddresses.push({ + address: addr.address, + publicKey: addr.publicKey, + derivationPath: '', + }); + }); + } + resolve(walletAddresses); + }) + .catch(reject); + }); + } + + sign(tx: Tx): Promise { + const xverseTx = tx as XverseTx; + return new Promise((resolve, reject) => { + const signInputs: Record = {}; + xverseTx.inputs.forEach((input: { address: string | number; idx: number; }) => { + signInputs[input.address] = [input.idx]; + }); + const signPsbtOptions = { + psbt: xverseTx.base64UnsignedPsbt, + signInputs, + broadcast: false, + }; + Wallet.request('signPsbt', signPsbtOptions) + .then((response) => { + if (response.status === 'error') { + reject(new Error(response.error.message)); + } else { + const signedPsbt = bitcoin.Psbt.fromBase64(response.result.psbt as string); + if (!signedPsbt.validateSignaturesOfAllInputs()) { + reject(new Error('Invalid signature provided')); + } else { + resolve({ + signedTx: signedPsbt.finalizeAllInputs().extractTransaction().toHex(), + }); + } + } + }) + .catch(() => reject(new Error('Unable to sign transaction'))); + }); + } + + isConnected(): Promise { + return Promise.resolve(true); + } + + reconnect(): Promise { + return new Promise((resolve, reject) => { + this.getAccountAddresses() + .then(() => resolve()) + .catch(reject); + }); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + getXpub(accountType: BtcAccount, accountNumber: number): Promise { + throw new Error('Method not supported.'); + } + + areEnoughUnusedAddresses(): boolean { + return this.addressesToFetch.segwit.lastIndex >= 1; + } + + availableAccounts(): BtcAccount[] { + return [constants.BITCOIN_SEGWIT_ADDRESS]; + } + + name(): Record<'formal_name' | 'short_name' | 'long_name', string> { + return constants.WALLET_NAMES.XVERSE; + } + + confirmationSteps(): Step[] { + return [ + { + title: 'Transaction information', + subtitle: '', + outputsToshow: { + opReturn: { + value: false, + amount: true, + }, + change: { + address: true, + amount: true, + }, + federation: { + address: true, + amount: true, + }, + }, + fullAmount: false, + fee: true, + }, + ]; + } +} diff --git a/src/common/services/index.ts b/src/common/services/index.ts index 620bd4a6..954228b3 100644 --- a/src/common/services/index.ts +++ b/src/common/services/index.ts @@ -4,3 +4,4 @@ export { default as TrezorService } from './TrezorService'; export { default as LedgerService } from './LedgerService'; export { default as LiqualityService } from './LiqualityService'; export { default as LeatherService } from './LeatherService'; +export { default as XverseService } from './XverseService'; diff --git a/src/common/store/constants.ts b/src/common/store/constants.ts index b93fe7c7..c77688f3 100644 --- a/src/common/store/constants.ts +++ b/src/common/store/constants.ts @@ -4,6 +4,7 @@ export const WALLET_NAMES = { LIQUALITY: { formal_name: 'Liquality', short_name: 'liquality', long_name: 'WALLET_LIQUALITY' }, METAMASK: { formal_name: 'Metamask', short_name: 'metamask', long_name: 'WALLET_METAMASK' }, LEATHER: { formal_name: 'Leather', short_name: 'leather', long_name: 'WALLET_LEATHER' }, + XVERSE: { formal_name: 'XVerse', short_name: 'xverse', long_name: 'WALLET_XVERSE' }, } as const; export const OPERATION_TYPE = 'OPERATION_TYPE'; diff --git a/src/common/types/Common.ts b/src/common/types/Common.ts index 9fc98cec..86b91534 100644 --- a/src/common/types/Common.ts +++ b/src/common/types/Common.ts @@ -106,6 +106,7 @@ export interface PsbtExtendedInput { value: number; script: Buffer; }; + redeemScript?: Buffer; } export interface NormalizedSummary { diff --git a/src/common/types/pegInTx.ts b/src/common/types/pegInTx.ts index a1121031..4b6a9e22 100644 --- a/src/common/types/pegInTx.ts +++ b/src/common/types/pegInTx.ts @@ -7,7 +7,7 @@ export type BtcAccount = 'BITCOIN_LEGACY_ADDRESS' | 'BITCOIN_NATIVE_SEGWIT_ADDRESS'; export type BtcWallet = 'WALLET_LEDGER' | 'WALLET_TREZOR' -| 'WALLET_LIQUALITY' | 'WALLET_LEATHER'; +| 'WALLET_LIQUALITY' | 'WALLET_LEATHER' | 'WALLET_XVERSE'; export type MiningSpeedFee = 'BITCOIN_SLOW_FEE_LEVEL' | 'BITCOIN_AVERAGE_FEE_LEVEL' | diff --git a/src/common/utils/btcAddressUtils.ts b/src/common/utils/btcAddressUtils.ts index 73ea1668..f013f8dc 100644 --- a/src/common/utils/btcAddressUtils.ts +++ b/src/common/utils/btcAddressUtils.ts @@ -52,3 +52,17 @@ export function validateAddress(address: string): {valid: boolean; addressType: } return { valid, addressType }; } + +function compressPublicKey(pubKey: string) { + const { publicKey } = bitcoin.ECPair.fromPublicKey(Buffer.from(pubKey, 'hex')); + return publicKey.toString('hex'); +} + +export function getP2SHRedeemScript(publicKey: string, network: bitcoin.Network) { + const pubkey = compressPublicKey(publicKey); + const pair = bitcoin.ECPair.fromPublicKey(Buffer.from(pubkey, 'hex')); + const p2wpkh = bitcoin.payments.p2wpkh({ pubkey: pair.publicKey, network }); + const p2sh = bitcoin.payments.p2sh({ redeem: p2wpkh, network }); + const redeem = p2sh.redeem?.output; + return redeem; +} diff --git a/src/common/walletConf.json b/src/common/walletConf.json index 4f933953..59a9cf09 100644 --- a/src/common/walletConf.json +++ b/src/common/walletConf.json @@ -39,6 +39,16 @@ "pegout": false, "hover": false, "btnClass": "btn-leather" + }, + { + "name": "Xverse", + "icon": "wallet-icons/xverse.png", + "iconWhite": "wallet-icons/xverse-white.png", + "constant": "WALLET_XVERSE", + "pegin": true, + "pegout": false, + "hover": false, + "btnClass": "btn-xverse" } ] } \ No newline at end of file diff --git a/src/pegin/components/create/SendBitcoin.vue b/src/pegin/components/create/SendBitcoin.vue index 0ad9731b..7aec8a9e 100644 --- a/src/pegin/components/create/SendBitcoin.vue +++ b/src/pegin/components/create/SendBitcoin.vue @@ -54,6 +54,7 @@ import TxErrorDialog from '@/common/components/exchange/TxErrorDialog.vue'; import { BridgeService } from '@/common/services/BridgeService'; import { TrezorError } from '@/common/types/exception/TrezorError'; import LeatherTxBuilder from '@/pegin/middleware/TxBuilder/LeatherTxBuilder'; +import XverseTxBuilder from '@/pegin/middleware/TxBuilder/XverseTxBuilder'; import PeginTxService from '../../services/PeginTxService'; export default defineComponent({ @@ -203,6 +204,10 @@ export default defineComponent({ txBuilder.value = new LeatherTxBuilder(); currentWallet.value = constants.WALLET_NAMES.LEATHER.short_name; break; + case constants.WALLET_NAMES.XVERSE.long_name: + txBuilder.value = new XverseTxBuilder(); + currentWallet.value = constants.WALLET_NAMES.XVERSE.short_name; + break; default: txBuilder.value = new TrezorTxBuilder(); break; diff --git a/src/pegin/middleware/TxBuilder/XverseTxBuilder.ts b/src/pegin/middleware/TxBuilder/XverseTxBuilder.ts new file mode 100644 index 00000000..486bc0e3 --- /dev/null +++ b/src/pegin/middleware/TxBuilder/XverseTxBuilder.ts @@ -0,0 +1,83 @@ +import { ApiService } from '@/common/services'; +import store from '@/common/store'; +import { + NormalizedInput, NormalizedTx, PsbtExtendedInput, Tx, +} from '@/common/types'; +import * as bitcoin from 'bitcoinjs-lib'; +import * as constants from '@/common/store/constants'; +import { getP2SHRedeemScript } from '@/common/utils'; +import TxBuilder from './TxBuilder'; + +export interface XverseTx extends Tx { + base64UnsignedPsbt: string; + inputs: Array<{idx: number; address: string}>; +} + +export default class XverseTxBuilder extends TxBuilder { + buildTx(normalizedTx: NormalizedTx): Promise { + console.log('XverseTxBuilder.buildTx()'); + return new Promise((resolve, reject) => { + const psbt = new bitcoin.Psbt({ network: this.network }); + this.getExtendedInputs(normalizedTx.inputs) + .then((extendedInputs) => { + psbt.addInputs(extendedInputs); + normalizedTx.outputs.forEach((normalizedOutput) => { + if (normalizedOutput.op_return_data) { + const buffer = Buffer.from(normalizedOutput.op_return_data, 'hex'); + const script: bitcoin.Payment = bitcoin.payments.embed({ data: [buffer] }); + if (script.output) { + psbt.addOutput({ + script: script.output, + value: 0, + }); + } + } else if (normalizedOutput.address) { + psbt.addOutput({ + address: normalizedOutput.address, + value: Number(normalizedOutput.amount), + }); + } + }); + const inputs = normalizedTx.inputs + .map((input) => ({ + address: input.address, + idx: input.prev_index, + })); + resolve({ + coin: this.coin, + inputs, + outputs: normalizedTx.outputs, + base64UnsignedPsbt: psbt.toBase64(), + }); + }) + .catch(reject); + }); + } + + private getExtendedInputs(normalizedInputs: Array) + :Promise> { + return new Promise>((resolve, reject) => { + const psbtExtendedInputs: Array = []; + const hexUtxoPromises = normalizedInputs + .map((input) => ApiService.getTxHex(input.prev_hash)); + Promise.all(hexUtxoPromises) + .then((hexUtxos) => { + normalizedInputs.forEach((normalizedInput, idx) => { + const utxo = bitcoin.Transaction.fromHex(hexUtxos[idx]); + const pubKey = store.getters[`pegInTx/${constants.PEGIN_TX_GET_ADDRESS_PUBLIC_KEY}`](normalizedInput.address); + psbtExtendedInputs.push({ + hash: normalizedInput.prev_hash, + index: normalizedInput.prev_index, + witnessUtxo: { + value: utxo.outs[normalizedInput.prev_index].value, + script: utxo.outs[normalizedInput.prev_index].script, + }, + redeemScript: getP2SHRedeemScript(pubKey, this.network), + }); + }); + resolve(psbtExtendedInputs); + }) + .catch(reject); + }); + } +} diff --git a/src/pegin/store/actions.ts b/src/pegin/store/actions.ts index 5dfdc645..0dfaa8fb 100644 --- a/src/pegin/store/actions.ts +++ b/src/pegin/store/actions.ts @@ -5,6 +5,7 @@ import * as constants from '@/common/store/constants'; import { ApiService, LedgerService, LiqualityService, TrezorService, LeatherService, + XverseService, } from '@/common/services'; import SatoshiBig from '@/common/types/SatoshiBig'; import { EnvironmentAccessorService } from '@/common/services/enviroment-accessor.service'; @@ -45,6 +46,9 @@ export const actions: ActionTree = { case constants.WALLET_NAMES.LEATHER.long_name: commit(constants.PEGIN_TX_SET_WALLET_SERVICE, new LeatherService()); break; + case constants.WALLET_NAMES.XVERSE.long_name: + commit(constants.PEGIN_TX_SET_WALLET_SERVICE, new XverseService()); + break; default: commit(constants.PEGIN_TX_SET_WALLET_SERVICE, undefined); break; diff --git a/src/pegin/store/getters.ts b/src/pegin/store/getters.ts index 2f29b080..5dcbfc83 100644 --- a/src/pegin/store/getters.ts +++ b/src/pegin/store/getters.ts @@ -21,6 +21,9 @@ export const getters: GetterTree = { case constants.WALLET_NAMES.LEATHER.long_name: { return constants.WALLET_NAMES.LEATHER.formal_name; } + case constants.WALLET_NAMES.XVERSE.long_name: { + return constants.WALLET_NAMES.XVERSE.formal_name; + } default: { return 'wallet'; } @@ -170,6 +173,9 @@ export const getters: GetterTree = { case constants.WALLET_NAMES.LEATHER.long_name: isHdWallet = false; break; + case constants.WALLET_NAMES.XVERSE.long_name: + isHdWallet = false; + break; default: isHdWallet = false; break; @@ -191,6 +197,9 @@ export const getters: GetterTree = { case constants.WALLET_NAMES.LEATHER.long_name: isSfWallet = true; break; + case constants.WALLET_NAMES.XVERSE.long_name: + isSfWallet = true; + break; default: isSfWallet = false; break; diff --git a/src/sats-connect.d.ts b/src/sats-connect.d.ts new file mode 100644 index 00000000..0bc21510 --- /dev/null +++ b/src/sats-connect.d.ts @@ -0,0 +1,103 @@ +declare module 'sats-connect' { + + export type AddressPurpose = 'ordinals' | 'payment'; + export enum BitcoinNetworkType { 'Mainnet', 'Testnet' } + + interface MethodParamsAndResult { + params: TParams; + result: TResult; + } + + interface Address { + address: string; + publicKey: string; + purpose?: AddressPurpose; + addressType?: AddressType; + } + + type GetAddressesParams = { + /** + * The purposes for which to generate addresses. + * possible values are "payment", "ordinals", ... + */ + purposes: Array; + /** + * a message to be displayed to the user in the request prompt. + */ + message?: string; + }; + + type GetAddressesResult = { + addresses: Array
; + }; + + type SignPsbtParams = { + /** + * The base64 encoded PSBT to sign. + */ + psbt: string; + /** + * The inputs to sign. + * The key is the address and the value is an array of indexes of the inputs to sign. + */ + signInputs: Record; + /** + * the sigHash type to use for signing. + * will default to the sighash type of the input if not provided. + * */ + allowedSignHash?: number; + /** + * Whether to broadcast the transaction after signing. + * */ + broadcast?: boolean; + }; + type SignPsbtResult = { + /** + * The base64 encoded PSBT after signing. + */ + psbt: string; + /** + * The transaction id as a hex-encoded string. + * This is only returned if the transaction was broadcast. + * */ + txid?: string; + }; + + type GetAddresses = MethodParamsAndResult; + type SignPsbt = MethodParamsAndResult; + + interface BtcRequests { + getAddresses: GetAddresses; + signPsbt: SignPsbt; + } + + export type Response = { + status: 'error'; + error: { + message: string; + }; + }| { + status: 'success'; + result: { + addresses: { + address: string; + publicKey: string; + }[]; + }; + } + + type Params = Method extends keyof BtcRequests ? BtcRequests[Method]['params'] : never; + + type RpcResult = { + result: RpcSuccessResponse['result']; + status: 'success'; + } | { + error: RpcErrorResponse['error']; + status: 'error'; + }; + + export default class Wallet { + static request(method: Method, params: Params) + : Promise>; + } +} From 414c3f73cd588e5d6d5aa38fec9030690b1c33f3 Mon Sep 17 00:00:00 2001 From: ronaldsg Date: Thu, 25 Apr 2024 15:25:58 -0500 Subject: [PATCH 2/5] Add jest support for mts files compile I've fixed the compilation error on the typescript files used by sats-connect updating the jest-config --- jest.config.js | 4 ++ package-lock.json | 10 ++-- src/sats-connect.d.ts | 103 ------------------------------------------ 3 files changed, 10 insertions(+), 107 deletions(-) delete mode 100644 src/sats-connect.d.ts diff --git a/jest.config.js b/jest.config.js index 86f530e0..3b41df82 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,4 +1,8 @@ module.exports = { + transform: { + '^.+\\.vue$': '@vue/vue3-jest', + '^.+\\.(mts|mjs|jsx|ts|tsx)$': 'ts-jest', + }, preset: '@vue/cli-plugin-unit-jest/presets/typescript-and-babel', collectCoverage: true, collectCoverageFrom: ['src/(common|pegin)/(providers|services|utils)/*.ts'], diff --git a/package-lock.json b/package-lock.json index 51617492..2c3380a1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6334,8 +6334,9 @@ }, "node_modules/@vue/vue3-jest": { "version": "27.0.0", + "resolved": "https://registry.npmjs.org/@vue/vue3-jest/-/vue3-jest-27.0.0.tgz", + "integrity": "sha512-VL61CgZBoQqayXfzlZJHHpZuX4lsT8dmdZMJzADhdAJjKu26JBpypHr/2ppevxItljPiuALQW4MKhhCXZRXnLg==", "dev": true, - "license": "MIT", "dependencies": { "@babel/plugin-transform-modules-commonjs": "^7.2.0", "chalk": "^2.1.0", @@ -8263,7 +8264,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001523", + "version": "1.0.30001612", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001612.tgz", + "integrity": "sha512-lFgnZ07UhaCcsSZgWW0K5j4e69dK1u/ltrL9lTUiFOwNHs12S3UMIEYgBV0Z6C6hRDev7iRnMzzYmKabYdXF9g==", "funding": [ { "type": "opencollective", @@ -8277,8 +8280,7 @@ "type": "github", "url": "https://github.com/sponsors/ai" } - ], - "license": "CC-BY-4.0" + ] }, "node_modules/case-sensitive-paths-webpack-plugin": { "version": "2.4.0", diff --git a/src/sats-connect.d.ts b/src/sats-connect.d.ts deleted file mode 100644 index 0bc21510..00000000 --- a/src/sats-connect.d.ts +++ /dev/null @@ -1,103 +0,0 @@ -declare module 'sats-connect' { - - export type AddressPurpose = 'ordinals' | 'payment'; - export enum BitcoinNetworkType { 'Mainnet', 'Testnet' } - - interface MethodParamsAndResult { - params: TParams; - result: TResult; - } - - interface Address { - address: string; - publicKey: string; - purpose?: AddressPurpose; - addressType?: AddressType; - } - - type GetAddressesParams = { - /** - * The purposes for which to generate addresses. - * possible values are "payment", "ordinals", ... - */ - purposes: Array; - /** - * a message to be displayed to the user in the request prompt. - */ - message?: string; - }; - - type GetAddressesResult = { - addresses: Array
; - }; - - type SignPsbtParams = { - /** - * The base64 encoded PSBT to sign. - */ - psbt: string; - /** - * The inputs to sign. - * The key is the address and the value is an array of indexes of the inputs to sign. - */ - signInputs: Record; - /** - * the sigHash type to use for signing. - * will default to the sighash type of the input if not provided. - * */ - allowedSignHash?: number; - /** - * Whether to broadcast the transaction after signing. - * */ - broadcast?: boolean; - }; - type SignPsbtResult = { - /** - * The base64 encoded PSBT after signing. - */ - psbt: string; - /** - * The transaction id as a hex-encoded string. - * This is only returned if the transaction was broadcast. - * */ - txid?: string; - }; - - type GetAddresses = MethodParamsAndResult; - type SignPsbt = MethodParamsAndResult; - - interface BtcRequests { - getAddresses: GetAddresses; - signPsbt: SignPsbt; - } - - export type Response = { - status: 'error'; - error: { - message: string; - }; - }| { - status: 'success'; - result: { - addresses: { - address: string; - publicKey: string; - }[]; - }; - } - - type Params = Method extends keyof BtcRequests ? BtcRequests[Method]['params'] : never; - - type RpcResult = { - result: RpcSuccessResponse['result']; - status: 'success'; - } | { - error: RpcErrorResponse['error']; - status: 'error'; - }; - - export default class Wallet { - static request(method: Method, params: Params) - : Promise>; - } -} From cdd7655e5fbaafb01cc48abf0ba610c2dfd0555e Mon Sep 17 00:00:00 2001 From: ronaldsg Date: Fri, 26 Apr 2024 00:27:31 -0500 Subject: [PATCH 3/5] Adding test for Xverse service --- src/common/services/XverseService.ts | 2 +- .../common/services/XverseService.spec.ts | 138 ++++++++++++++++++ 2 files changed, 139 insertions(+), 1 deletion(-) create mode 100644 tests/unit/common/services/XverseService.spec.ts diff --git a/src/common/services/XverseService.ts b/src/common/services/XverseService.ts index ef2dff19..56db074e 100644 --- a/src/common/services/XverseService.ts +++ b/src/common/services/XverseService.ts @@ -80,7 +80,7 @@ export default class XverseService extends WalletService { } } }) - .catch(() => reject(new Error('Unable to sign transaction'))); + .catch(() => reject(new Error('Invalid psbt provided'))); }); } diff --git a/tests/unit/common/services/XverseService.spec.ts b/tests/unit/common/services/XverseService.spec.ts new file mode 100644 index 00000000..1a8c68ed --- /dev/null +++ b/tests/unit/common/services/XverseService.spec.ts @@ -0,0 +1,138 @@ +import { EnvironmentAccessorService } from '@/common/services/enviroment-accessor.service'; +import * as constants from '@/common/store/constants'; +import sinon from 'sinon'; +import Wallet from 'sats-connect'; +import { WalletService, XverseService } from '@/common/services'; + +const initEnvironment = () => { + const defaultEnvironmentVariables = { + vueAppCoin: constants.BTC_NETWORK_TESTNET, + vueAppManifestAppUrl: '', + vueAppManifestEmail: '', + vueAppWalletAddressPerCall: 5, + vueAppWalletAddressHardStop: 100, + }; + EnvironmentAccessorService.initializeEnvironmentVariables(defaultEnvironmentVariables); +}; + +describe('Xverse Service:', () => { + // let request: sinon.SinonStub<[XverseRequestArgs], Promise>; + // let mockedXverseProvider: sinon.SinonStubbedInstance; + + beforeEach(initEnvironment); + afterEach(() => sinon.restore()); + + it('should create a XverseService instance', () => { + const xverseService = new XverseService(); + expect(xverseService).toBeInstanceOf(XverseService); + expect(xverseService).toBeInstanceOf(WalletService); + }); + + it('should return a single address since that is the current amount supported by xverse', () => { + sinon.stub(Wallet, 'request').resolves({ + status: 'success', + result: { + addresses: [ + { + address: 'testAddress', + publicKey: 'testPublicKey', + }, + ], + }, + }); + const xverseService = new XverseService(); + xverseService.getAccountAddresses().then((addresses) => { + expect(addresses.length).toBe(1); + expect(addresses[0].address).toBe('testAddress'); + expect(addresses[0].publicKey).toBe('testPublicKey'); + }); + }); + + it('should handle the error case when the wallet are not available', () => { + sinon.stub(Wallet, 'request').resolves({ + status: 'error', + error: { + code: 1, + message: 'Wallet not available', + }, + }); + const xverseService = new XverseService(); + expect(xverseService.getAccountAddresses()) + .rejects + .toThrow('Wallet not available'); + }); + + it('should return an error when the psbt are wrong', () => { + sinon.stub(Wallet, 'request').resolves({ + status: 'success', + result: { + psbt: 'cHNidP8BAHECAAAAAW4TCBaK74DxafvrRdWpF32Gg5eVRs1DJX9YHz2v9jduAQAAAAD9', + }, + }); + const xverseService = new XverseService(); + const xverseTx = { + coin: 'test', + inputs: [ + { + addres: '2Mxv1YkAXpTMcq2at1it9QRfq8bDX82N99J', + idx: 2, + }, + ], + outputs: [ + { + amount: '0', + op_return_data: '52534b5401aFf12FA1c482BEab1D70C68fe0Fc5825447A9818', + }, + { + address: '2N3JQb9erL1SnAr3NTMrZiPQQ8dcjJp4idV', + amount: '500000', + }, + { + address: '2Mxv1YkAXpTMcq2at1it9QRfq8bDX82N99J', + amount: '982474', + }, + ], + base64UnsignedPsbt: 'cHNidP8BAJcCAAAAAboaf2woNcottY/Ax+9lbYi349++WmdwYZPyOp8nXg5tAgAAAAD/////AwAAAAAAAAAAG2oZUlNLVAGv8S+hxIK+qx1wxo/g/FglRHqYGCChBwAAAAAAF6kUbkta6F2G5NsObl2wn4wnYyjNvz+Hyv0OAAAAAAAXqRQ+Lnd+D2z9GnchcMB/v3/pWn8CfYcAAAAAAAEBIL60FgAAAAAAF6kUPi53fg9s/Rp3IXDAf79/6Vp/An2HAQQWABSmO24c7Zf0uweZC7P9rNOrMR7sqwAAAAA=', + }; + expect(xverseService.sign(xverseTx)) + .rejects + .toThrow('Invalid psbt provided'); + }); + + it('should return an error when the psbt has some unsigned input', () => { + sinon.stub(Wallet, 'request').resolves({ + status: 'success', + result: { + psbt: 'cHNidP8BAJcCAAAAAboaf2woNcottY/Ax+9lbYi349++WmdwYZPyOp8nXg5tAgAAAAD/////AwAAAAAAAAAAG2oZUlNLVAGv8S+hxIK+qx1wxo/g/FglRHqYGCChBwAAAAAAF6kUbkta6F2G5NsObl2wn4wnYyjNvz+Hyv0OAAAAAAAXqRQ+Lnd+D2z9GnchcMB/v3/pWn8CfYcAAAAAAAEBIL60FgAAAAAAF6kUPi53fg9s/Rp3IXDAf79/6Vp/An2HAQQWABSmO24c7Zf0uweZC7P9rNOrMR7sqwAAAAA=', + }, + }); + const xverseService = new XverseService(); + const xverseTx = { + coin: 'test', + inputs: [ + { + addres: '2Mxv1YkAXpTMcq2at1it9QRfq8bDX82N99J', + idx: 2, + }, + ], + outputs: [ + { + amount: '0', + op_return_data: '52534b5401aFf12FA1c482BEab1D70C68fe0Fc5825447A9818', + }, + { + address: '2N3JQb9erL1SnAr3NTMrZiPQQ8dcjJp4idV', + amount: '500000', + }, + { + address: '2Mxv1YkAXpTMcq2at1it9QRfq8bDX82N99J', + amount: '982474', + }, + ], + base64UnsignedPsbt: 'cHNidP8BAJcCAAAAAboaf2woNcottY/Ax+9lbYi349++WmdwYZPyOp8nXg5tAgAAAAD/////AwAAAAAAAAAAG2oZUlNLVAGv8S+hxIK+qx1wxo/g/FglRHqYGCChBwAAAAAAF6kUbkta6F2G5NsObl2wn4wnYyjNvz+Hyv0OAAAAAAAXqRQ+Lnd+D2z9GnchcMB/v3/pWn8CfYcAAAAAAAEBIL60FgAAAAAAF6kUPi53fg9s/Rp3IXDAf79/6Vp/An2HAQQWABSmO24c7Zf0uweZC7P9rNOrMR7sqwAAAAA=', + }; + expect(xverseService.sign(xverseTx)) + .rejects + .toThrow('Invalid psbt provided'); + }); +}); From 36f6b669bc0ccaa6b35d570c578ca65d5fd81ed4 Mon Sep 17 00:00:00 2001 From: Ronald Sarmiento Date: Fri, 3 May 2024 12:42:46 -0500 Subject: [PATCH 4/5] Update src/common/services/XverseService.ts Co-authored-by: lserra-iov <117093501+lserra-iov@users.noreply.github.com> --- src/common/services/XverseService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/services/XverseService.ts b/src/common/services/XverseService.ts index 56db074e..21aeeff8 100644 --- a/src/common/services/XverseService.ts +++ b/src/common/services/XverseService.ts @@ -27,7 +27,7 @@ export default class XverseService extends WalletService { return new Promise((resolve, reject) => { const walletAddresses: WalletAddress[] = []; const payload = { - purposes: ['ordinals', 'payment'] as AddressPurpose[], + purposes: ['payment'] as AddressPurpose[], message: 'Welcome to the 2wp-app, please select your Bitcoin account to start.', network: { type: this.satsBtcNetwork, From 03f0c29cf41913a89acfc9f5eed5e8fed68b885f Mon Sep 17 00:00:00 2001 From: ronaldsg Date: Mon, 6 May 2024 13:47:18 -0500 Subject: [PATCH 5/5] Fix signing inputs collection on xverse I've updated the signingInput collection on the xverse service in order to select the inputs indexes of the created tx instead of the utxo. Reported by --- src/common/services/XverseService.ts | 8 ++++++-- src/pegin/middleware/TxBuilder/XverseTxBuilder.ts | 1 - 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/common/services/XverseService.ts b/src/common/services/XverseService.ts index 21aeeff8..44b4add6 100644 --- a/src/common/services/XverseService.ts +++ b/src/common/services/XverseService.ts @@ -57,8 +57,12 @@ export default class XverseService extends WalletService { const xverseTx = tx as XverseTx; return new Promise((resolve, reject) => { const signInputs: Record = {}; - xverseTx.inputs.forEach((input: { address: string | number; idx: number; }) => { - signInputs[input.address] = [input.idx]; + xverseTx.inputs.forEach((input: { address: string; idx: number; }, inputIdx) => { + if (signInputs[input.address]) { + signInputs[input.address].push(inputIdx); + } else { + signInputs[input.address] = [inputIdx]; + } }); const signPsbtOptions = { psbt: xverseTx.base64UnsignedPsbt, diff --git a/src/pegin/middleware/TxBuilder/XverseTxBuilder.ts b/src/pegin/middleware/TxBuilder/XverseTxBuilder.ts index 486bc0e3..f36c80cf 100644 --- a/src/pegin/middleware/TxBuilder/XverseTxBuilder.ts +++ b/src/pegin/middleware/TxBuilder/XverseTxBuilder.ts @@ -15,7 +15,6 @@ export interface XverseTx extends Tx { export default class XverseTxBuilder extends TxBuilder { buildTx(normalizedTx: NormalizedTx): Promise { - console.log('XverseTxBuilder.buildTx()'); return new Promise((resolve, reject) => { const psbt = new bitcoin.Psbt({ network: this.network }); this.getExtendedInputs(normalizedTx.inputs)