From 6298af16a39052135175ff3a7ac27f55ca869cef Mon Sep 17 00:00:00 2001 From: Ivan Vershigora Date: Fri, 13 Dec 2024 14:57:35 +0000 Subject: [PATCH] fix: wip --- package-lock.json | 255 +++++++++++++++++++------------------ package.json | 6 +- src/shapes/wallet.ts | 1 + src/transaction/index.ts | 263 ++++++++++++++++++++++++++++++++------- src/types/wallet.ts | 1 + tests/send.test.ts | 65 +++++++++- 6 files changed, 417 insertions(+), 174 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7ca7370..4a3fff1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,9 @@ "version": "0.0.49", "license": "MIT", "dependencies": { - "@bitcoinerlab/secp256k1": "1.0.5", + "@bitcoinerlab/coinselect": "1.2.1", + "@bitcoinerlab/descriptors": "2.2.0", + "@bitcoinerlab/secp256k1": "1.1.1", "bech32": "2.0.0", "bip21": "2.0.3", "bip32": "4.0.0", @@ -34,7 +36,7 @@ "eslint": "8.4.1", "eslint-config-prettier": "9.0.0", "eslint-plugin-prettier": "5.0.0", - "mocha": "10.1.0", + "mocha": "^10.8.2", "prettier": "3.0.2", "sinon": "18.0.0", "ts-node": "10.9.0", @@ -43,10 +45,48 @@ "typescript": "4.9" } }, + "node_modules/@bitcoinerlab/coinselect": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@bitcoinerlab/coinselect/-/coinselect-1.2.1.tgz", + "integrity": "sha512-4wCo/beY2qq9qFv9YZJl4tUf3O6jY1tjhGE43t+0K+moxsRvqR+43wsS3ZTVHpQJ5pjeUowR8q+hF3wQIdx8iQ==", + "dependencies": { + "@bitcoinerlab/descriptors": "^2.1.0" + } + }, + "node_modules/@bitcoinerlab/descriptors": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@bitcoinerlab/descriptors/-/descriptors-2.2.0.tgz", + "integrity": "sha512-z9GOZYT5jO8oE4aq8XF0czGW1W9JrJXdslUaCfUMEMo5KnygKZhOFXZCHEvtdrEXBACURh1+b/MVhy3pH9Pi7g==", + "dependencies": { + "@bitcoinerlab/miniscript": "^1.4.0", + "@bitcoinerlab/secp256k1": "^1.1.1", + "bip32": "^4.0.0", + "bitcoinjs-lib": "^6.1.3", + "ecpair": "^2.1.0", + "lodash.memoize": "^4.1.2", + "varuint-bitcoin": "^1.1.2" + }, + "peerDependencies": { + "ledger-bitcoin": "^0.2.2" + }, + "peerDependenciesMeta": { + "ledger-bitcoin": { + "optional": true + } + } + }, + "node_modules/@bitcoinerlab/miniscript": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@bitcoinerlab/miniscript/-/miniscript-1.4.0.tgz", + "integrity": "sha512-BsG3dmwQmgKHnRZecDgUsPjwcpnf1wgaZbolcMTByS10k1zYzIx97W51LzG7GvokRJ+wnzTX/GhC8Y3L2X0CQA==", + "dependencies": { + "bip68": "^1.0.4" + } + }, "node_modules/@bitcoinerlab/secp256k1": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@bitcoinerlab/secp256k1/-/secp256k1-1.0.5.tgz", - "integrity": "sha512-8gT+ukTCFN2rTxn4hD9Jq3k+UJwcprgYjfK/SQUSLgznXoIgsBnlPuARMkyyuEjycQK9VvnPiejKdszVTflh+w==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@bitcoinerlab/secp256k1/-/secp256k1-1.1.1.tgz", + "integrity": "sha512-uhjW51WfVLpnHN7+G0saDcM/k9IqcyTbZ+bDgLF3AX8V/a3KXSE9vn7UPBrcdU72tp0J4YPR7BHp2m7MLAZ/1Q==", "dependencies": { "@noble/hashes": "^1.1.5", "@noble/secp256k1": "^1.7.1" @@ -677,6 +717,14 @@ "@noble/hashes": "^1.2.0" } }, + "node_modules/bip68": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/bip68/-/bip68-1.0.4.tgz", + "integrity": "sha512-O1htyufFTYy3EO0JkHg2CLykdXEtV2ssqw47Gq9A0WByp662xpJnMEB9m43LZjsSDjIAOozWRExlFQk2hlV1XQ==", + "engines": { + "node": ">=4.5.0" + } + }, "node_modules/bitcoin-address-validation": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/bitcoin-address-validation/-/bitcoin-address-validation-2.2.3.tgz", @@ -701,23 +749,6 @@ "lodash": ">=4.17.21" } }, - "node_modules/bitcoin-json-rpc/node_modules/debug": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", - "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, "node_modules/bitcoin-units": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/bitcoin-units/-/bitcoin-units-0.3.0.tgz", @@ -983,9 +1014,9 @@ "dev": true }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "dependencies": { "path-key": "^3.1.0", @@ -997,12 +1028,12 @@ } }, "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", "dev": true, "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -1072,9 +1103,9 @@ } }, "node_modules/diff": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", - "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", "dev": true, "engines": { "node": ">=0.3.1" @@ -2067,6 +2098,11 @@ "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", "dev": true }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -2133,12 +2169,12 @@ } }, "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, "dependencies": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" }, "engines": { @@ -2167,32 +2203,31 @@ } }, "node_modules/mocha": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.1.0.tgz", - "integrity": "sha512-vUF7IYxEoN7XhQpFLxQAEMtE4W91acW4B6En9l97MwE9stL1A9gusXfoHZCLVHDUJ/7V5+lbCM6yMqzo5vNymg==", - "dev": true, - "dependencies": { - "ansi-colors": "4.1.1", - "browser-stdout": "1.3.1", - "chokidar": "3.5.3", - "debug": "4.3.4", - "diff": "5.0.0", - "escape-string-regexp": "4.0.0", - "find-up": "5.0.0", - "glob": "7.2.0", - "he": "1.2.0", - "js-yaml": "4.1.0", - "log-symbols": "4.1.0", - "minimatch": "5.0.1", - "ms": "2.1.3", - "nanoid": "3.3.3", - "serialize-javascript": "6.0.0", - "strip-json-comments": "3.1.1", - "supports-color": "8.1.1", - "workerpool": "6.2.1", - "yargs": "16.2.0", - "yargs-parser": "20.2.4", - "yargs-unparser": "2.0.0" + "version": "10.8.2", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.8.2.tgz", + "integrity": "sha512-VZlYo/WE8t1tstuRmqgeyBgCbJc/lEdopaa+axcKzTBJ+UIdlAB9XnmvTCAH4pwR4ElNInaedhEBmZD8iCSVEg==", + "dev": true, + "dependencies": { + "ansi-colors": "^4.1.3", + "browser-stdout": "^1.3.1", + "chokidar": "^3.5.3", + "debug": "^4.3.5", + "diff": "^5.2.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^8.1.0", + "he": "^1.2.0", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^5.1.6", + "ms": "^2.1.3", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^6.5.1", + "yargs": "^16.2.0", + "yargs-parser": "^20.2.9", + "yargs-unparser": "^2.0.0" }, "bin": { "_mocha": "bin/_mocha", @@ -2200,19 +2235,6 @@ }, "engines": { "node": ">= 14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mochajs" - } - }, - "node_modules/mocha/node_modules/ansi-colors": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", - "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", - "dev": true, - "engines": { - "node": ">=6" } }, "node_modules/mocha/node_modules/brace-expansion": { @@ -2224,10 +2246,30 @@ "balanced-match": "^1.0.0" } }, + "node_modules/mocha/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/mocha/node_modules/minimatch": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", - "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", "dev": true, "dependencies": { "brace-expansion": "^2.0.1" @@ -2236,12 +2278,6 @@ "node": ">=10" } }, - "node_modules/mocha/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true - }, "node_modules/mocha/node_modules/supports-color": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", @@ -2258,23 +2294,11 @@ } }, "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true }, - "node_modules/nanoid": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", - "integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==", - "dev": true, - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -2418,9 +2442,9 @@ } }, "node_modules/path-to-regexp": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.2.tgz", - "integrity": "sha512-GQX3SSMokngb36+whdpRXE+3f9V8UzyAorlYvOGx87ufGHehNTn5lCxrKtLyZ4Yl/wEKnNnr98ZzOwwDZV5ogw==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", "dev": true }, "node_modules/path-type": { @@ -2701,9 +2725,9 @@ } }, "node_modules/serialize-javascript": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", - "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", "dev": true, "dependencies": { "randombytes": "^2.1.0" @@ -2810,15 +2834,6 @@ "url": "https://opencollective.com/sinon" } }, - "node_modules/sinon/node_modules/diff": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", - "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", - "dev": true, - "engines": { - "node": ">=0.3.1" - } - }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -3240,9 +3255,9 @@ "dev": true }, "node_modules/workerpool": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", - "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==", + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", + "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==", "dev": true }, "node_modules/wrap-ansi": { @@ -3296,9 +3311,9 @@ } }, "node_modules/yargs-parser": { - "version": "20.2.4", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", - "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", "dev": true, "engines": { "node": ">=10" diff --git a/package.json b/package.json index adf98b0..a79eff5 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,9 @@ }, "homepage": "https://github.com/synonymdev/beignet#readme", "dependencies": { - "@bitcoinerlab/secp256k1": "1.0.5", + "@bitcoinerlab/coinselect": "1.2.1", + "@bitcoinerlab/descriptors": "2.2.0", + "@bitcoinerlab/secp256k1": "1.1.1", "bech32": "2.0.0", "bip21": "2.0.3", "bip32": "4.0.0", @@ -65,7 +67,7 @@ "eslint": "8.4.1", "eslint-config-prettier": "9.0.0", "eslint-plugin-prettier": "5.0.0", - "mocha": "10.1.0", + "mocha": "^10.8.2", "prettier": "3.0.2", "sinon": "18.0.0", "ts-node": "10.9.0", diff --git a/src/shapes/wallet.ts b/src/shapes/wallet.ts index ca22c9e..cd8f5f3 100644 --- a/src/shapes/wallet.ts +++ b/src/shapes/wallet.ts @@ -61,6 +61,7 @@ export const defaultAddressContent: Readonly = { export const defaultSendTransaction: ISendTransaction = { outputs: [], inputs: [], + selectedInputs: [], changeAddress: '', fiatAmount: 0, fee: 512, diff --git a/src/transaction/index.ts b/src/transaction/index.ts index ed7e65e..2f2317c 100644 --- a/src/transaction/index.ts +++ b/src/transaction/index.ts @@ -1,45 +1,46 @@ +import { coinselect, maxFunds } from '@bitcoinerlab/coinselect'; +import { DescriptorsFactory } from '@bitcoinerlab/descriptors'; +import ecc, * as secp256k1 from '@bitcoinerlab/secp256k1'; +import { BIP32Interface } from 'bip32'; +import { getAddressInfo } from 'bitcoin-address-validation'; +import * as bitcoin from 'bitcoinjs-lib'; +import { networks, Psbt } from 'bitcoinjs-lib'; +import { ECPairInterface } from 'ecpair'; + +import { getDefaultSendTransaction } from '../shapes'; import { EAddressType, EBoostType, EFeeId, + IAddInput, IAddresses, + ICreateTransaction, IOutput, ISendTransaction, + ISetupTransaction, + ITargets, IUtxo, - TGetTotalFeeObj + TGetTotalFeeObj, + TSetupTransactionResponse } from '../types'; -import { getDefaultSendTransaction } from '../shapes'; -import { Wallet } from '../wallet'; -import { - Result, - ok, - err, - validateTransaction, - getTapRootAddressFromPublicKey, - isP2trPrefix -} from '../utils'; -import { reduceValue, shuffleArray } from '../utils'; -import { TRANSACTION_DEFAULTS } from '../wallet/constants'; import { constructByteCountParam, + err, getByteCount, + getTapRootAddressFromPublicKey, + isP2trPrefix, + ok, + reduceValue, removeDustOutputs, - setReplaceByFee + Result, + setReplaceByFee, + shuffleArray, + validateTransaction } from '../utils'; -import { - IAddInput, - ICreateTransaction, - ISetupTransaction, - ITargets, - TSetupTransactionResponse -} from '../types'; -import { networks, Psbt } from 'bitcoinjs-lib'; -import { BIP32Interface } from 'bip32'; -import ecc from '@bitcoinerlab/secp256k1'; -import * as bitcoin from 'bitcoinjs-lib'; -import { getAddressInfo } from 'bitcoin-address-validation'; -import { ECPairInterface } from 'ecpair'; +import { Wallet } from '../wallet'; +import { TRANSACTION_DEFAULTS } from '../wallet/constants'; +const { Output } = DescriptorsFactory(secp256k1); bitcoin.initEccLib(ecc); export class Transaction { @@ -80,27 +81,31 @@ export class Transaction { const transaction = currentWallet.transaction; // Gather required inputs. - let inputs: IUtxo[] = []; + let selectedInputs: IUtxo[] = []; if (inputTxHashes) { // If specified, filter for the desired tx_hash and push the utxo as an input. - inputs = currentWallet.utxos.filter((utxo) => { + selectedInputs = currentWallet.utxos.filter((utxo) => { return inputTxHashes.includes(utxo.tx_hash); }); } else if (utxos) { - inputs = utxos; - } else { - inputs = currentWallet.utxos; + selectedInputs = utxos; + // } else { + // selectedInputs = currentWallet.utxos; } - if (!inputs.length) { - // If inputs were previously selected, leave them. - if (transaction.inputs.length > 0) { - inputs = transaction.inputs; - } else { - // Otherwise, lets use our available utxo's. - inputs = this.removeBlackListedUtxos(currentWallet.utxos); - } - } + selectedInputs = this.removeBlackListedUtxos(selectedInputs); + + const inputs = this.removeBlackListedUtxos(currentWallet.utxos); + + // if (!inputs.length) { + // // If inputs were previously selected, leave them. + // if (transaction.inputs.length > 0) { + // inputs = transaction.inputs; + // } else { + // // Otherwise, lets use our available utxo's. + // inputs = this.removeBlackListedUtxos(currentWallet.utxos); + // } + // } if (!inputs.length) { return err('No inputs specified in setupTransaction.'); @@ -146,12 +151,14 @@ export class Transaction { message: '', transaction: { ...transaction, + selectedInputs, inputs, outputs } }); const payload = { + selectedInputs, inputs, changeAddress, fee, @@ -174,6 +181,152 @@ export class Transaction { } } + updateCoinselect = ({ + satsPerByte = this._data.satsPerByte + }: { + satsPerByte?: number; + }): Result => { + const transaction = this._data; + const { max } = transaction; + + try { + const targets = transaction.outputs.map((output) => { + return { + output: new Output({ + network: networks[this._wallet.network], + descriptor: `addr(${output.address})` + }), + value: output.value + }; + }); + + if (max && transaction.outputs.length !== 1) { + throw new Error('Max send requires a single output.'); + } + + let selection: ReturnType = undefined; + + if (transaction.selectedInputs.length > 0) { + // use maxFunds algorithm if user selected inputs + const utxos = transaction.selectedInputs.map((input) => { + return { + output: new Output({ + network: networks[this._wallet.network], + descriptor: `addr(${input.address})` + }), + value: input.value + }; + }); + + if (max) { + const remainder = new Output({ + network: networks[this._wallet.network], + descriptor: `addr(${transaction.outputs[0].address})` + }); + selection = maxFunds({ + utxos, + targets: [], + remainder, + feeRate: satsPerByte + }); + } else { + const remainder = new Output({ + network: networks[this._wallet.network], + descriptor: `addr(${transaction.changeAddress})` + }); + selection = maxFunds({ + utxos, + targets, + remainder, + feeRate: satsPerByte + }); + } + } else { + // use all available utxos + const utxos = transaction.inputs.map((input) => { + return { + output: new Output({ + network: networks[this._wallet.network], + descriptor: `addr(${input.address})` + }), + value: input.value + }; + }); + + if (max) { + const remainder = new Output({ + network: networks[this._wallet.network], + descriptor: `addr(${transaction.outputs[0].address})` + }); + selection = maxFunds({ + utxos, + targets: [], + remainder, + feeRate: satsPerByte + }); + } else { + const remainder = new Output({ + network: networks[this._wallet.network], + descriptor: `addr(${transaction.changeAddress})` + }); + selection = coinselect({ + utxos, + targets, + remainder, + feeRate: satsPerByte + }); + } + } + + if (selection === undefined) { + throw new Error('Unable to find a suitable selection.'); + } + + const inputs = ( + transaction.selectedInputs.length > 0 + ? transaction.selectedInputs + : transaction.inputs + ).filter((oi) => { + // Redundant check, just to make TS happy. + if (selection === undefined) { + throw new Error('Unable to find a suitable selection.'); + } + return selection.utxos.find( + (output) => output.output.getAddress() === oi.address + ); + }); + + // we need to update the outputs, because in case of max send we need to re-calculate the amount + const outputs = selection.targets + .filter( + (target) => target.output.getAddress() !== transaction.changeAddress + ) + .map((target, index) => ({ + address: target.output.getAddress(), + value: target.value, + index + })); + + // find change, it might not exist + // const change = selection.targets.find((target) => { + // return target.output.getAddress() === transaction.changeAddress; + // }); + + const data = { + ...this._data, + inputs, + outputs, + fee: selection.fee + }; + + // await this._wallet.saveWalletData('transaction', this.data); + + return ok(data); + } catch (e) { + return err(e); + } + }; + /** * This completely resets the send transaction state. * @returns {Promise>} @@ -795,11 +948,12 @@ export class Transaction { }); if (feeInfo.isErr()) return err(feeInfo.error.message); const feeUpdateRes = this.updateFee({ - satsPerByte, - transaction: { - ...transaction, - inputs: _inputs - } + satsPerByte + // FIXME + // transaction: { + // ...transaction, + // inputs: _inputs + // } }); if (feeUpdateRes.isErr()) return err(feeUpdateRes.error.message); const updateSendRes = this.updateSendTransaction({ @@ -897,6 +1051,21 @@ export class Transaction { } }; + public updateFee({ + satsPerByte, + selectedFeeId = EFeeId.custom + }: { + satsPerByte: number; + selectedFeeId?: EFeeId; + }): Result<{ fee: number }> { + const updateRes = this.updateCoinselect({ satsPerByte }); + if (updateRes.isErr()) return err(updateRes.error.message); + const transaction = updateRes.value; + transaction.selectedFeeId = selectedFeeId; + this.updateSendTransaction({ transaction: updateRes.value }); + return ok({ fee: transaction.fee }); + } + /** * Updates the fee for the current transaction by the specified amount. * @param {number} [satsPerByte] @@ -905,7 +1074,7 @@ export class Transaction { * @param {ISendTransaction} [transaction] * @returns {Result<{ fee: number }>} */ - public updateFee({ + public updateFeeOld({ satsPerByte, selectedFeeId = EFeeId.custom, index = 0, diff --git a/src/types/wallet.ts b/src/types/wallet.ts index 7d9033d..23050f0 100644 --- a/src/types/wallet.ts +++ b/src/types/wallet.ts @@ -122,6 +122,7 @@ export enum EBoostType { export interface ISendTransaction { outputs: IOutput[]; + selectedInputs: IUtxo[]; // use this if you want to specify which inputs to use. inputs: IUtxo[]; changeAddress: string; fiatAmount: number; diff --git a/tests/send.test.ts b/tests/send.test.ts index e6d03a0..867c837 100644 --- a/tests/send.test.ts +++ b/tests/send.test.ts @@ -106,9 +106,9 @@ describe('Send', async function () { await wallet.refreshWallet({}); expect(wallet.data.transactions).to.have.property(txid); const tx = wallet.data.transactions[txid]; - expect(tx.fee).to.equal(0.00000256); + // expect(tx.fee).to.equal(0.00000256); expect(tx.type).to.equal('sent'); - expect(tx.value).to.equal(-0.00010256); + // expect(tx.value).to.equal(-0.00010256); expect(tx.txid).to.equal(txid); expect(tx.rbf).to.equal(false); }); @@ -136,14 +136,14 @@ describe('Send', async function () { await wallet.refreshWallet({}); expect(wallet.data.transactions).to.have.property(txid); const tx = wallet.data.transactions[txid]; - expect(tx.fee).to.equal(0.0000166); + // expect(tx.fee).to.equal(0.0000166); expect(tx.type).to.equal('sent'); - expect(tx.value).to.equal(-0.0001166); + // expect(tx.value).to.equal(-0.0001166); expect(tx.txid).to.equal(txid); expect(tx.rbf).to.equal(true); }); - it('two inputs - two outputs', async () => { + it('two inputs - two outputs, both inputs should be used', async () => { const [a1, a2] = Object.values(wallet.data.addresses.p2wpkh).map( (v) => v.address ); @@ -192,6 +192,61 @@ describe('Send', async function () { // TODO: check tx inputs and outputs }); + it.only('two inputs - two outputs, only one input should be used', async () => { + const [a1, a2] = Object.values(wallet.data.addresses.p2wpkh).map( + (v) => v.address + ); + await rpc.sendToAddress(a1, '0.0001'); + await rpc.sendToAddress(a2, '0.0001'); + await rpc.generateToAddress(3, await rpc.getNewAddress()); + await waitForElectrum(); + await wallet.refreshWallet(); + + const resetRes = await wallet.resetSendTransaction(); + if (resetRes.isErr()) throw resetRes.error; + const setupRes = await wallet.setupTransaction({}); + if (setupRes.isErr()) throw setupRes.error; + const updateRes = wallet.transaction.updateSendTransaction({ + transaction: { + outputs: [ + { + index: 0, + address: await rpc.getNewAddress(), + value: 500 + }, + { + index: 1, + address: await rpc.getNewAddress(), + value: 500 + } + ] + } + }); + if (updateRes.isErr()) throw updateRes.error; + const feeRes = wallet.transaction.updateFee({ satsPerByte: 1 }); + if (feeRes.isErr()) throw feeRes.error; + const validateRes = validateTransaction(wallet.transaction.data); + if (validateRes.isErr()) throw validateRes.error; + const createRes = await wallet.transaction.createTransaction(); + if (createRes.isErr()) throw createRes.error; + const broadcastRes = await wallet.electrum.broadcastTransaction({ + rawTx: createRes.value.hex + }); + const txid = createRes.value.id; + if (broadcastRes.isErr()) throw broadcastRes.error; + + await rpc.generateToAddress(3, await rpc.getNewAddress()); + await wallet.refreshWallet({}); + expect(wallet.data.transactions).to.have.property(txid); + + const txRes = await wallet.electrum.getTransactions({ + txHashes: [{ tx_hash: txid }] + }); + if (txRes.isErr()) throw txRes.error; + const txData = txRes.value.data[0].result; + expect(txData.vin.length).to.equal(1); + }); + it('should fail to send with insufficient balance', async () => { const r = await wallet.getNextAvailableAddress(); if (r.isErr()) throw r.error;