diff --git a/package.json b/package.json index 03c7ede..ca7c67a 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,8 @@ { "name": "multichain-tools", - "version": "3.0.1", + "version": "3.0.2", "description": "", - "main": "index.js", - "typings": "index.d.ts", + "main": "./src/index.ts", "scripts": { "build": "tsc --project tsconfig.json", "pre:deploy": "npm run build && cp package.json README.md dist/", @@ -18,7 +17,6 @@ "@babel/preset-env": "^7.25.3", "@babel/preset-typescript": "^7.24.7", "@types/bn.js": "^5.1.5", - "@types/lodash.pickby": "^4.6.9", "@typescript-eslint/eslint-plugin": "^6.21.0", "babel-jest": "^29.7.0", "dotenv": "^16.4.5", @@ -43,18 +41,17 @@ "@near-js/accounts": "^1.3.0", "@near-js/crypto": "^1.4.0", "@near-js/keystores": "^0.2.0", - "@near-js/transactions": "^1.3.0", + "@near-js/transactions": "^1.3.1", + "@near-wallet-selector/core": "^8.9.5", "axios": "^1.6.8", "bech32": "^2.0.0", "bitcoinjs-lib": "^6.1.5", "bn.js": "^5.2.1", "bs58": "^6.0.0", - "canonicalize": "^2.0.0", "chain-registry": "^1.63.103", "coinselect": "^3.1.13", "cosmjs-types": "^0.9.0", "ethers": "^6.11.1", - "lodash.pickby": "^4.6.0", "near-api-js": "^3.0.4" }, "packageManager": "pnpm@9.14.2+sha512.6e2baf77d06b9362294152c851c4f278ede37ab1eba3a55fda317a4a17b209f4dbb973fb250a77abc463a341fcb1f17f17cfa24091c4eb319cda0d9b84278387" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f086a24..d43caab 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -33,8 +33,11 @@ importers: specifier: ^0.2.0 version: 0.2.0 '@near-js/transactions': - specifier: ^1.3.0 - version: 1.3.0 + specifier: ^1.3.1 + version: 1.3.1 + '@near-wallet-selector/core': + specifier: ^8.9.5 + version: 8.9.14(near-api-js@3.0.4) axios: specifier: ^1.6.8 version: 1.7.7 @@ -50,9 +53,6 @@ importers: bs58: specifier: ^6.0.0 version: 6.0.0 - canonicalize: - specifier: ^2.0.0 - version: 2.0.0 chain-registry: specifier: ^1.63.103 version: 1.63.103 @@ -65,9 +65,6 @@ importers: ethers: specifier: ^6.11.1 version: 6.13.2 - lodash.pickby: - specifier: ^4.6.0 - version: 4.6.0 near-api-js: specifier: ^3.0.4 version: 3.0.4 @@ -84,9 +81,6 @@ importers: '@types/bn.js': specifier: ^5.1.5 version: 5.1.6 - '@types/lodash.pickby': - specifier: ^4.6.9 - version: 4.6.9 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.2))(eslint@8.57.1)(typescript@5.6.2) @@ -924,6 +918,9 @@ packages: '@near-js/crypto@1.4.0': resolution: {integrity: sha512-2SYS7LyFz2/y8idqAyyS4jf3pn6zFg4tLbOq9OlB+MTZhvsnUcWW+HLznyBytp6dW8lAQ03E+Ew0bYfJSCIJJw==} + '@near-js/crypto@1.4.1': + resolution: {integrity: sha512-hbricJD0H8nwu63Zw16UZQg3ms2W9NwDBsLt3OEtudTcu9q1MRrVZWc7ATjdmTvhkcgmouEFc6oLBsOxnmSLCA==} + '@near-js/keystores-browser@0.0.9': resolution: {integrity: sha512-JzPj+RHJN2G3CEm/LyfbtZDQy/wxgOlqfh52voqPGijUHg93b27KBqtZShazAgJNkhzRbWcoluWQnd2jL8vF7A==} @@ -936,6 +933,9 @@ packages: '@near-js/keystores@0.2.0': resolution: {integrity: sha512-vZiyx9whLlA7/EDdkZGf//0AL2FWAUyGpVhWIHcbJZwQ7DNcjpkb0tRydFp8Yk4bb7kcYnoyksSeRx9kQUMyjA==} + '@near-js/keystores@0.2.1': + resolution: {integrity: sha512-KTeqSB+gx5LZNC9VGtHDe+aEiJts6e3nctMnnn/gqIgvW7KJ+BzcmTZZpxCmQLcy+s7hHSpzmyTVRkaCuYjCcQ==} + '@near-js/providers@0.1.1': resolution: {integrity: sha512-0M/Vz2Ac34ShKVoe2ftVJ5Qg4eSbEqNXDbCDOdVj/2qbLWZa7Wpe+me5ei4TMY2ZhGdawhgJUPrYwdJzOCyf8w==} @@ -948,27 +948,45 @@ packages: '@near-js/signers@0.2.0': resolution: {integrity: sha512-plzTnjI7IodTtMwGe2m1bg1ZwGeHeKanJqVoXFypZj7gOuuqVOi+9vcHdSu7T2McnzRujPQbj31PmfDQ3O3YCw==} + '@near-js/signers@0.2.1': + resolution: {integrity: sha512-l1PnUy4e8NQe5AAHs7mEuWbbUt0rrsZLtcK1UlFaA16MKZmxXdHLMBfUmzyMA4bGzwkyUyGtIebkR+KjBfpEog==} + '@near-js/transactions@1.1.2': resolution: {integrity: sha512-AqYA56ncwgrWjIu+bNaWjTPRZb0O+SfpWIP7U+1FKNKxNYMCtkt6zp7SlQeZn743shKVq9qMzA9+ous/KCb0QQ==} '@near-js/transactions@1.3.0': resolution: {integrity: sha512-M9DuFX009E5twEbPV9Fs67nNu8T8segE7yG57q02MmPMOQ7RDanHA2fKqARsltTZ26EEXb92x3lAKt7qFdCfCw==} + '@near-js/transactions@1.3.1': + resolution: {integrity: sha512-kL9hxUqBr+tILQHFsh5T/bz3UkJrAq5tnyFqh0xf+7qGXZuRIPfuW/HMq4M6wFw0MGi/8ycmDT3yTQFH7PzZqw==} + '@near-js/types@0.0.4': resolution: {integrity: sha512-8TTMbLMnmyG06R5YKWuS/qFG1tOA3/9lX4NgBqQPsvaWmDsa+D+QwOkrEHDegped0ZHQwcjAXjKML1S1TyGYKg==} '@near-js/types@0.3.0': resolution: {integrity: sha512-IwayA5Wa4+hryo22AuAYIu5a/nOAheF/Bmz9kpuouX9L4he+Tc8xAt5NfE60zXG7tsukAw1QAaHE1kBzhmwtKw==} + '@near-js/types@0.3.1': + resolution: {integrity: sha512-8qIA7ynAEAuVFNAQc0cqz2xRbfyJH3PaAG5J2MgPPhD18lu/tCGd6pzYg45hjhtiJJRFDRjh/FUWKS+ZiIIxUw==} + '@near-js/utils@0.1.0': resolution: {integrity: sha512-kOVAXmJzaC8ElJD3RLEoBuqOK+d5s7jc0JkvhyEtbuEmXYHHAy9Q17/YkDcX9tyr01L85iOt66z0cODqzgtQwA==} '@near-js/utils@1.0.0': resolution: {integrity: sha512-4dd6fDgWZnG+0VSKPBA3czEQdi9UotepdwcEKLTbXepIL1FX2ZlQV6HVi7KYmrAVwv1ims11vGnWzJWKy46ULw==} + '@near-js/utils@1.0.1': + resolution: {integrity: sha512-MzCAspVJJLrURnSbq059s6cWon2/qbbBVl+Ib1yBOMTs/6EuJ7GRvuSmtmSB7l9Hjjmz8Imn1aB2q3RVYZSbrA==} + '@near-js/wallet-account@1.1.1': resolution: {integrity: sha512-NnoJKtogBQ7Qz+AP+LdF70BP8Az6UXQori7OjPqJLMo73bn6lh5Ywvegwd1EB7ZEVe4BRt9+f9QkbU5M8ANfAw==} + '@near-wallet-selector/core@8.9.14': + resolution: {integrity: sha512-fC+igD832P0wUZ9MSfFWgYVIvZcOn1Fka7SJ3VN7w8Xufw+MgZ06zoGDj29QB0qy+Aui1fZGJgwJGTBOe5KIMg==} + peerDependencies: + '@near-js/providers': latest + near-api-js: 4.0.3 + '@noble/curves@1.2.0': resolution: {integrity: sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==} @@ -1087,12 +1105,6 @@ packages: '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} - '@types/lodash.pickby@4.6.9': - resolution: {integrity: sha512-SPI248FYnyd3jOxDeJq2vX2UKQnDzqacuqdeOVqwE1MPSk8gN8TA3FcHSMQWLlpBnuHgXvgKInvywbOFbidpJA==} - - '@types/lodash@4.17.9': - resolution: {integrity: sha512-w9iWudx1XWOHW5lQRS9iKpK/XuRhnN+0T7HvdCCd802FYkT1AMTnxndJHGrNJwRoRHkslGr4S29tjm1cT7x/7w==} - '@types/long@4.0.2': resolution: {integrity: sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==} @@ -1354,6 +1366,9 @@ packages: bn.js@5.2.1: resolution: {integrity: sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==} + borsh@0.7.0: + resolution: {integrity: sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA==} + borsh@1.0.0: resolution: {integrity: sha512-fSVWzzemnyfF89EPwlUNsrS5swF5CrtiN4e+h0/lLf4dz2he4L3ndM20PS9wj7ICSkXJe/TQUHdaPTq15b1mNQ==} @@ -1419,9 +1434,6 @@ packages: caniuse-lite@1.0.30001664: resolution: {integrity: sha512-AmE7k4dXiNKQipgn7a2xg558IRqPN3jMQY/rOsbxDhrd0tyChwbITBfiwtnqz8bi2M5mIWbxAYBvk7W7QBUS2g==} - canonicalize@2.0.0: - resolution: {integrity: sha512-ulDEYPv7asdKvqahuAY35c1selLdzDwHqugK92hfkzvlDCwXRRelDkR+Er33md/PtnpqHemgkuDPanZ4fiYZ8w==} - chain-registry@1.63.103: resolution: {integrity: sha512-TICiOH+lqHhFvyTaM1y7WWAPaJswz8NhASG24AkenoZnkaZMEAdwUCiscC7H6/BloDA3FMQvAgDeTG3HycGvKA==} @@ -1790,6 +1802,10 @@ packages: resolution: {integrity: sha512-9VkriTTed+/27BGuY1s0hf441kqwHJ1wtN2edksEtiRvXx+soxRX3iSXTfFqq2+YwrOqbDoTHjIhQnjJRlzKmg==} engines: {node: '>=14.0.0'} + events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + execa@5.1.1: resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} engines: {node: '>=10'} @@ -2301,6 +2317,9 @@ packages: node-notifier: optional: true + js-sha256@0.9.0: + resolution: {integrity: sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA==} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -2387,9 +2406,6 @@ packages: lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} - lodash.pickby@4.6.0: - resolution: {integrity: sha512-AZV+GsS/6ckvPOVQPXSiFFacKvKB4kOQu6ynt9wz0F3LO4R9Ij4K1ddYsIytDpSgLz88JHd9P+oaLeej5/Sl7Q==} - long@4.0.0: resolution: {integrity: sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==} @@ -2711,6 +2727,9 @@ packages: run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + rxjs@7.8.1: + resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} + safe-array-concat@1.1.2: resolution: {integrity: sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==} engines: {node: '>=0.4'} @@ -2853,6 +2872,9 @@ packages: resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} engines: {node: '>=8'} + text-encoding-utf-8@1.0.2: + resolution: {integrity: sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg==} + text-table@0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} @@ -4271,6 +4293,15 @@ snapshots: randombytes: 2.1.0 secp256k1: 5.0.0 + '@near-js/crypto@1.4.1': + dependencies: + '@near-js/types': 0.3.1 + '@near-js/utils': 1.0.1 + '@noble/curves': 1.2.0 + borsh: 1.0.0 + randombytes: 2.1.0 + secp256k1: 5.0.0 + '@near-js/keystores-browser@0.0.9': dependencies: '@near-js/crypto': 1.2.1 @@ -4291,6 +4322,11 @@ snapshots: '@near-js/crypto': 1.4.0 '@near-js/types': 0.3.0 + '@near-js/keystores@0.2.1': + dependencies: + '@near-js/crypto': 1.4.1 + '@near-js/types': 0.3.1 + '@near-js/providers@0.1.1': dependencies: '@near-js/transactions': 1.1.2 @@ -4329,6 +4365,12 @@ snapshots: '@near-js/keystores': 0.2.0 '@noble/hashes': 1.3.3 + '@near-js/signers@0.2.1': + dependencies: + '@near-js/crypto': 1.4.1 + '@near-js/keystores': 0.2.1 + '@noble/hashes': 1.3.3 + '@near-js/transactions@1.1.2': dependencies: '@near-js/crypto': 1.2.1 @@ -4348,12 +4390,23 @@ snapshots: '@noble/hashes': 1.3.3 borsh: 1.0.0 + '@near-js/transactions@1.3.1': + dependencies: + '@near-js/crypto': 1.4.1 + '@near-js/signers': 0.2.1 + '@near-js/types': 0.3.1 + '@near-js/utils': 1.0.1 + '@noble/hashes': 1.3.3 + borsh: 1.0.0 + '@near-js/types@0.0.4': dependencies: bn.js: 5.2.1 '@near-js/types@0.3.0': {} + '@near-js/types@0.3.1': {} + '@near-js/utils@0.1.0': dependencies: '@near-js/types': 0.0.4 @@ -4369,6 +4422,13 @@ snapshots: depd: 2.0.0 mustache: 4.0.0 + '@near-js/utils@1.0.1': + dependencies: + '@near-js/types': 0.3.1 + bs58: 4.0.0 + depd: 2.0.0 + mustache: 4.0.0 + '@near-js/wallet-account@1.1.1': dependencies: '@near-js/accounts': 1.0.4 @@ -4383,6 +4443,14 @@ snapshots: transitivePeerDependencies: - encoding + '@near-wallet-selector/core@8.9.14(near-api-js@3.0.4)': + dependencies: + borsh: 0.7.0 + events: 3.3.0 + js-sha256: 0.9.0 + near-api-js: 3.0.4 + rxjs: 7.8.1 + '@noble/curves@1.2.0': dependencies: '@noble/hashes': 1.3.2 @@ -4493,12 +4561,6 @@ snapshots: '@types/json5@0.0.29': {} - '@types/lodash.pickby@4.6.9': - dependencies: - '@types/lodash': 4.17.9 - - '@types/lodash@4.17.9': {} - '@types/long@4.0.2': {} '@types/node@18.15.13': {} @@ -4838,6 +4900,12 @@ snapshots: bn.js@5.2.1: {} + borsh@0.7.0: + dependencies: + bn.js: 5.2.1 + bs58: 4.0.0 + text-encoding-utf-8: 1.0.2 + borsh@1.0.0: {} brace-expansion@1.1.11: @@ -4907,8 +4975,6 @@ snapshots: caniuse-lite@1.0.30001664: {} - canonicalize@2.0.0: {} - chain-registry@1.63.103: dependencies: '@chain-registry/types': 0.45.83 @@ -5357,6 +5423,8 @@ snapshots: - bufferutil - utf-8-validate + events@3.3.0: {} + execa@5.1.1: dependencies: cross-spawn: 7.0.3 @@ -6070,6 +6138,8 @@ snapshots: - supports-color - ts-node + js-sha256@0.9.0: {} + js-tokens@4.0.0: {} js-yaml@3.14.1: @@ -6136,8 +6206,6 @@ snapshots: lodash.merge@4.6.2: {} - lodash.pickby@4.6.0: {} - long@4.0.0: {} lru-cache@5.1.1: @@ -6453,6 +6521,10 @@ snapshots: dependencies: queue-microtask: 1.2.3 + rxjs@7.8.1: + dependencies: + tslib: 2.7.0 + safe-array-concat@1.1.2: dependencies: call-bind: 1.0.7 @@ -6599,6 +6671,8 @@ snapshots: glob: 7.2.3 minimatch: 3.1.2 + text-encoding-utf-8@1.0.2: {} + text-table@0.2.0: {} tmpl@1.0.5: {} diff --git a/src/chains/Bitcoin/Bitcoin.ts b/src/chains/Bitcoin/Bitcoin.ts index 65bbad3..a2796d9 100644 --- a/src/chains/Bitcoin/Bitcoin.ts +++ b/src/chains/Bitcoin/Bitcoin.ts @@ -1,40 +1,48 @@ import axios from 'axios' import * as bitcoin from 'bitcoinjs-lib' +import { fetchBTCFeeProperties, parseBTCNetwork } from './utils' import { - fetchBTCFeeProperties, - fetchDerivedBTCAddressAndPublicKey, - parseBTCNetwork, -} from './utils' -import { type ChainSignatureContracts, type NearAuthentication } from '../types' -import { type KeyDerivationPath } from '../../kdf/types' + type MPCPayloads, + type ChainSignatureContracts, + type NearNetworkIds, +} from '../types' import { type BTCNetworkIds, - type BTCTransaction, type UTXO, type BTCOutput, type Transaction, type BTCAddressInfo, + type BTCTransactionRequest, + type BTCUnsignedTransaction, } from './types' -import { toRSV } from '../../signature/utils' -import { type RSVSignature, type MPCSignature } from '../../signature/types' +import { toRSV, najToPubKey } from '../../signature/utils' +import { + type RSVSignature, + type MPCSignature, + type KeyDerivationPath, +} from '../../signature/types' +import { ChainSignaturesContract } from '../../contracts' +import { type Chain } from '../Chain' -export class Bitcoin { +export class Bitcoin + implements Chain +{ + private readonly nearNetworkId: NearNetworkIds private readonly network: BTCNetworkIds private readonly providerUrl: string private readonly contract: ChainSignatureContracts - private readonly signer: (txHash: Uint8Array) => Promise constructor(config: { + nearNetworkId: NearNetworkIds network: BTCNetworkIds providerUrl: string contract: ChainSignatureContracts - signer: (txHash: Uint8Array) => Promise }) { + this.nearNetworkId = config.nearNetworkId this.network = config.network this.providerUrl = config.providerUrl this.contract = config.contract - this.signer = config.signer } static toBTC(satoshis: number): number { @@ -45,16 +53,9 @@ export class Bitcoin { return Math.round(btc * 100000000) } - async fetchBalance(address: string): Promise { - const { data } = await axios.get( - `${this.providerUrl}/address/${address}` - ) - return Bitcoin.toBTC( - data.chain_stats.funded_txo_sum - data.chain_stats.spent_txo_sum - ).toString() - } - - async fetchTransaction(transactionId: string): Promise { + private async fetchTransaction( + transactionId: string + ): Promise { const { data } = await axios.get( `${this.providerUrl}/tx/${transactionId}` ) @@ -88,7 +89,7 @@ export class Bitcoin { return tx } - static parseRSVSignature(signature: RSVSignature): Buffer { + private static parseRSVSignature(signature: RSVSignature): Buffer { const r = signature.r.padStart(64, '0') const s = signature.s.padStart(64, '0') @@ -101,33 +102,13 @@ export class Bitcoin { return rawSignature } - async sendTransaction(txHex: string): Promise { - try { - const response = await axios.post(`${this.providerUrl}/tx`, txHex) - - if (response.status === 200) { - return response.data - } - throw new Error(`Failed to broadcast transaction: ${response.data}`) - } catch (error: unknown) { - console.error(error) - throw new Error(`Error broadcasting transaction`) - } - } - - async handleTransaction( - data: BTCTransaction, - nearAuthentication: NearAuthentication, - path: KeyDerivationPath - ): Promise { - const { address, publicKey } = await fetchDerivedBTCAddressAndPublicKey({ - signerId: nearAuthentication.accountId, - path, - btcNetworkId: this.network, - nearNetworkId: nearAuthentication.networkId, - multichainContractId: this.contract, - }) - + private async createPSBT({ + address, + data, + }: { + address: string + data: BTCTransactionRequest + }): Promise { const { inputs, outputs } = data.inputs && data.outputs ? data @@ -177,25 +158,163 @@ export class Bitcoin { } }) - const keyPair = { + return psbt + } + + async getBalance(address: string): Promise { + const { data } = await axios.get( + `${this.providerUrl}/address/${address}` + ) + return Bitcoin.toBTC( + data.chain_stats.funded_txo_sum - data.chain_stats.spent_txo_sum + ).toString() + } + + async deriveAddressAndPublicKey( + signerId: string, + path: KeyDerivationPath + ): Promise<{ + address: string + publicKey: string + }> { + const derivedPubKeyNAJ = await ChainSignaturesContract.getDerivedPublicKey({ + networkId: this.nearNetworkId, + contract: this.contract, + args: { path, predecessor: signerId }, + }) + + if (!derivedPubKeyNAJ) { + throw new Error('Failed to get derived public key') + } + + const derivedKey = najToPubKey(derivedPubKeyNAJ, { compress: true }) + const publicKeyBuffer = Buffer.from(derivedKey, 'hex') + const network = parseBTCNetwork(this.network) + + // Use P2WPKH (Bech32) address type + const payment = bitcoin.payments.p2wpkh({ + pubkey: publicKeyBuffer, + network, + }) + + const { address } = payment + + if (!address) { + throw new Error('Failed to generate Bitcoin address') + } + + return { address, publicKey: derivedKey } + } + + setTransaction( + transaction: BTCUnsignedTransaction, + storageKey: string + ): void { + window.localStorage.setItem( + storageKey, + JSON.stringify({ + psbt: transaction.psbt.toHex(), + publicKey: transaction.publicKey, + }) + ) + } + + getTransaction( + storageKey: string, + options?: { + remove?: boolean + } + ): BTCUnsignedTransaction | undefined { + const txSerialized = window.localStorage.getItem(storageKey) + if (options?.remove) { + window.localStorage.removeItem(storageKey) + } + const transactionJSON = txSerialized ? JSON.parse(txSerialized) : undefined + return transactionJSON + ? { + psbt: bitcoin.Psbt.fromHex(transactionJSON.psbt as string), + publicKey: transactionJSON.publicKey, + } + : undefined + } + + async getMPCPayloadAndTransaction( + transactionRequest: BTCTransactionRequest + ): Promise<{ + transaction: BTCUnsignedTransaction + mpcPayloads: MPCPayloads + }> { + const publicKey = Buffer.from(transactionRequest.publicKey, 'hex') + const psbt = await this.createPSBT({ + address: transactionRequest.from, + data: transactionRequest, + }) + + // Duplicate the psbt because we can't sign it twice + const psbtHex = psbt.toHex() + + const payloads: MPCPayloads = [] + // Mock signer to get the payloads as the library doesn't expose a methods with such functionality + const keyPair = (index: number): bitcoin.Signer => ({ publicKey, - sign: async (hash: Buffer): Promise => { - const mpcSignature = await this.signer(hash) - return Bitcoin.parseRSVSignature(toRSV(mpcSignature)) + sign: (hash) => { + payloads.push({ + index, + payload: new Uint8Array(hash), + }) + // The return it's intentionally wrong as this is a mock signer + return Buffer.from(new Array(64).fill(0)) + }, + }) + for (let index = 0; index < psbt.txInputs.length; index += 1) { + // TODO: check if you can double sign it, otherwise you will have to clone the psbt before signing + psbt.signInput(index, keyPair(index)) + } + + return { + transaction: { + psbt: bitcoin.Psbt.fromHex(psbtHex), + publicKey: transactionRequest.publicKey, }, + mpcPayloads: payloads.sort((a, b) => a.index - b.index), } + } - // Sign inputs sequentially to avoid nonce issues - for (let index = 0; index < inputs.length; index += 1) { - await psbt.signInputAsync(index, keyPair) + async addSignatureAndBroadcast({ + transaction: { psbt, publicKey }, + mpcSignatures, + }: { + transaction: BTCUnsignedTransaction + mpcSignatures: MPCSignature[] + }): Promise { + const publicKeyBuffer = Buffer.from(publicKey, 'hex') + const keyPair = (index: number): bitcoin.Signer => ({ + publicKey: publicKeyBuffer, + sign: () => { + const mpcSignature = mpcSignatures[index] + return Bitcoin.parseRSVSignature(toRSV(mpcSignature)) + }, + }) + for (let index = 0; index < psbt.txInputs.length; index += 1) { + psbt.signInput(index, keyPair(index)) } psbt.finalizeAllInputs() - const txid = await this.sendTransaction(psbt.extractTransaction().toHex()) - if (txid) { - return txid + try { + const response = await axios.post( + `${this.providerUrl}/tx`, + psbt.extractTransaction().toHex() + ) + + if (response.status === 200) { + return response.data + } + + throw new Error(`Failed to broadcast transaction: ${response.data}`) + } catch (error: unknown) { + console.error(error) + throw new Error(`Error broadcasting transaction`) } - throw new Error('Failed to broadcast transaction') } } diff --git a/src/chains/Bitcoin/types.ts b/src/chains/Bitcoin/types.ts index bb67c1e..72f3180 100644 --- a/src/chains/Bitcoin/types.ts +++ b/src/chains/Bitcoin/types.ts @@ -1,10 +1,6 @@ -import { type KeyDerivationPath } from '../../kdf/types' -import { - type ChainSignatureContracts, - type NearNetworkIds, - type ChainProvider, - type NearAuthentication, -} from '../types' +import { type KeyDerivationPath } from '../../signature' +import { type ChainProvider, type NearAuthentication } from '../types' +import type * as bitcoin from 'bitcoinjs-lib' export interface Transaction { txid: string @@ -72,7 +68,9 @@ interface BtcInputsAndOutputs { outputs: BTCOutput[] } -export type BTCTransaction = { +export type BTCTransactionRequest = { + publicKey: string + from: string to: string value: string } & ( @@ -83,12 +81,17 @@ export type BTCTransaction = { } ) +export interface BTCUnsignedTransaction { + psbt: bitcoin.Psbt + publicKey: string +} + export type BTCChainConfigWithProviders = ChainProvider & { network: BTCNetworkIds } export interface BitcoinRequest { - transaction: BTCTransaction + transaction: BTCTransactionRequest chainConfig: BTCChainConfigWithProviders nearAuthentication: NearAuthentication fastAuthRelayerUrl?: string @@ -97,14 +100,6 @@ export interface BitcoinRequest { export type BTCNetworkIds = 'mainnet' | 'testnet' | 'regtest' -export interface BitcoinPublicKeyAndAddressRequest { - signerId: string - path: KeyDerivationPath - btcNetworkId: BTCNetworkIds - nearNetworkId: NearNetworkIds - multichainContractId: ChainSignatureContracts -} - export interface BTCFeeRecommendation { fastestFee: number halfHourFee: number diff --git a/src/chains/Bitcoin/utils.ts b/src/chains/Bitcoin/utils.ts index 9980bb0..59f0ffa 100644 --- a/src/chains/Bitcoin/utils.ts +++ b/src/chains/Bitcoin/utils.ts @@ -6,15 +6,7 @@ import * as bitcoin from 'bitcoinjs-lib' // @ts-expect-error import coinselect from 'coinselect' -import { - type BTCOutput, - type BitcoinPublicKeyAndAddressRequest, - type UTXO, - type BTCFeeRecommendation, -} from './types' -import { getCanonicalizedDerivationPath } from '../../kdf/utils' -import { ChainSignaturesContract } from '../../signature/chain-signatures-contract' -import { najToPubKey } from '../../kdf/kdf' +import { type BTCOutput, type UTXO, type BTCFeeRecommendation } from './types' export async function fetchBTCFeeRate( providerUrl: string, @@ -74,45 +66,6 @@ export async function fetchBTCFeeProperties( return ret } -export async function fetchDerivedBTCAddressAndPublicKey({ - signerId, - path, - btcNetworkId, - nearNetworkId, - multichainContractId, -}: BitcoinPublicKeyAndAddressRequest): Promise<{ - address: string - publicKey: Buffer -}> { - const derivedPubKeyNAJ = await ChainSignaturesContract.getDerivedPublicKey({ - networkId: nearNetworkId, - contract: multichainContractId, - args: { path: getCanonicalizedDerivationPath(path), predecessor: signerId }, - }) - - if (!derivedPubKeyNAJ) { - throw new Error('Failed to get derived public key') - } - - const derivedKey = najToPubKey(derivedPubKeyNAJ, { compress: true }) - const publicKeyBuffer = Buffer.from(derivedKey, 'hex') - const network = parseBTCNetwork(btcNetworkId) - - // Use P2WPKH (Bech32) address type - const payment = bitcoin.payments.p2wpkh({ - pubkey: publicKeyBuffer, - network, - }) - - const { address } = payment - - if (!address) { - throw new Error('Failed to generate Bitcoin address') - } - - return { address, publicKey: publicKeyBuffer } -} - export function parseBTCNetwork(network: string): bitcoin.networks.Network { switch (network.toLowerCase()) { case 'mainnet': diff --git a/src/chains/Chain.ts b/src/chains/Chain.ts new file mode 100644 index 0000000..d3f9c00 --- /dev/null +++ b/src/chains/Chain.ts @@ -0,0 +1,56 @@ +import { type MPCSignature, type KeyDerivationPath } from '../signature/types' + +export interface Chain { + /** + * Gets the balance for a given address + */ + getBalance: (address: string) => Promise + + /** + * Derives an address and public key from a signer ID and derivation path + */ + deriveAddressAndPublicKey: ( + signerId: string, + path: KeyDerivationPath + ) => Promise<{ + address: string + publicKey: string + }> + + /** + * Stores a transaction in local storage + */ + setTransaction: (transaction: UnsignedTransaction, storageKey: string) => void + + /** + * Retrieves a transaction from local storage + */ + getTransaction: ( + storageKey: string, + options?: { + remove?: boolean + } + ) => UnsignedTransaction | undefined + + /** + * Gets the MPC payload and transaction for signing + */ + getMPCPayloadAndTransaction: ( + transactionRequest: TransactionRequest + ) => Promise<{ + transaction: UnsignedTransaction + mpcPayloads: Array<{ + index: number + payload: Uint8Array + }> + }> + + /** + * Adds signatures to transaction and broadcasts it + */ + addSignatureAndBroadcast: (params: { + transaction: UnsignedTransaction + mpcSignatures: MPCSignature[] + publicKey: string + }) => Promise +} diff --git a/src/chains/Cosmos/Cosmos.ts b/src/chains/Cosmos/Cosmos.ts index 35b9bdb..60117b4 100644 --- a/src/chains/Cosmos/Cosmos.ts +++ b/src/chains/Cosmos/Cosmos.ts @@ -13,113 +13,261 @@ import { } from '@cosmjs/proto-signing' import { type SignDoc } from 'cosmjs-types/cosmos/tx/v1beta1/tx' import { toBase64, fromHex } from '@cosmjs/encoding' -import { sha256 } from '@cosmjs/crypto' +import { ripemd160, sha256 } from '@cosmjs/crypto' -import { fetchChainInfo, fetchDerivedCosmosAddressAndPublicKey } from './utils' -import { type ChainSignatureContracts, type NearAuthentication } from '../types' -import { type KeyDerivationPath } from '../../kdf/types' -import { type CosmosTransaction, type CosmosNetworkIds } from './types' -import { type MPCSignature, type RSVSignature } from '../../signature/types' -import { toRSV } from '../../signature/utils' +import { fetchChainInfo } from './utils' +import { + type MPCPayloads, + type ChainSignatureContracts, + type NearNetworkIds, +} from '../types' +import { + type BalanceResponse, + type CosmosNetworkIds, + type CosmosTransactionRequest, + type CosmosUnsignedTransaction, +} from './types' +import { + type MPCSignature, + type RSVSignature, + type KeyDerivationPath, +} from '../../signature/types' +import { toRSV, najToPubKey } from '../../signature/utils' +import { ChainSignaturesContract } from '../../contracts' +import { type Chain } from '../Chain' +import { bech32 } from 'bech32' -export class Cosmos { +export class Cosmos + implements Chain +{ + private readonly nearNetworkId: NearNetworkIds private readonly registry: Registry private readonly contract: ChainSignatureContracts private readonly chainId: CosmosNetworkIds - private readonly signer: (txHash: Uint8Array) => Promise - // TODO: should include providerUrl, so the user can choose rpc constructor({ + nearNetworkId, contract, chainId, - signer, }: { + nearNetworkId: NearNetworkIds contract: ChainSignatureContracts chainId: CosmosNetworkIds - signer: (txHash: Uint8Array) => Promise }) { + this.nearNetworkId = nearNetworkId this.registry = new Registry() this.contract = contract this.chainId = chainId - this.signer = signer } - private async createSigner( - address: string, - publicKey: Uint8Array - ): Promise { - return { + private parseRSVSignature(rsvSignature: RSVSignature): Uint8Array { + return new Uint8Array([ + ...fromHex(rsvSignature.r), + ...fromHex(rsvSignature.s), + ]) + } + + async getBalance(address: string): Promise { + try { + const { restUrl, denom, decimals } = await fetchChainInfo(this.chainId) + + const response = await fetch( + `${restUrl}/cosmos/bank/v1beta1/balances/${address}` + ) + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`) + } + + const data = (await response.json()) as BalanceResponse + const balance = data.balances.find((b) => b.denom === denom) + const amount = balance?.amount ?? '0' + + const formattedBalance = ( + parseInt(amount) / Math.pow(10, decimals) + ).toString() + return formattedBalance + } catch (error) { + console.error('Failed to fetch Cosmos balance:', error) + throw new Error('Failed to fetch Cosmos balance') + } + } + + async deriveAddressAndPublicKey( + signerId: string, + path: KeyDerivationPath + ): Promise<{ + address: string + publicKey: string + }> { + const { prefix } = await fetchChainInfo(this.chainId) + const derivedPubKeyNAJ = await ChainSignaturesContract.getDerivedPublicKey({ + networkId: this.nearNetworkId, + contract: this.contract, + args: { path, predecessor: signerId }, + }) + + if (!derivedPubKeyNAJ) { + throw new Error('Failed to get derived public key') + } + + const derivedKey = najToPubKey(derivedPubKeyNAJ, { compress: true }) + const pubKeySha256 = sha256(Buffer.from(fromHex(derivedKey))) + const ripemd160Hash = ripemd160(pubKeySha256) + const address = bech32.encode(prefix, bech32.toWords(ripemd160Hash)) + + return { address, publicKey: derivedKey } + } + + setTransaction( + transaction: { + address: string + messages: EncodeObject[] + memo?: string + fee: StdFee + }, + storageKey: string + ): void { + window.localStorage.setItem(storageKey, JSON.stringify(transaction)) + } + + getTransaction( + storageKey: string, + options?: { + remove?: boolean + } + ): CosmosUnsignedTransaction | undefined { + const serializedTransaction = window.localStorage.getItem(storageKey) + if (options?.remove) { + window.localStorage.removeItem(storageKey) + } + return serializedTransaction ? JSON.parse(serializedTransaction) : undefined + } + + async getMPCPayloadAndTransaction( + transactionRequest: CosmosTransactionRequest + ): Promise<{ + transaction: CosmosUnsignedTransaction + mpcPayloads: MPCPayloads + }> { + const { denom, rpcUrl, gasPrice } = await fetchChainInfo(this.chainId) + const publicKeyBuffer = Buffer.from(transactionRequest.publicKey, 'hex') + + // Mock signer to get the payloads as the library doesn't expose a methods with such functionality + const payloads: Uint8Array[] = [] + const signer: OfflineDirectSigner = { getAccounts: async () => [ { - address, + address: transactionRequest.address, algo: 'secp256k1', - pubkey: publicKey, + pubkey: publicKeyBuffer, }, ], signDirect: async (signerAddress: string, signDoc: SignDoc) => { - if (signerAddress !== address) { + if (signerAddress !== transactionRequest.address) { throw new Error(`Address ${signerAddress} not found in wallet`) } const txHash = sha256(makeSignBytes(signDoc)) - const mpcSignature = await this.signer(txHash) - const signature = this.parseRSVSignature(toRSV(mpcSignature)) + payloads.push(txHash) return { signed: signDoc, signature: { pub_key: { type: 'tendermint/PubKeySecp256k1', - value: toBase64(publicKey), + value: toBase64(publicKeyBuffer), }, - signature: toBase64(signature), + // The return it's intentionally wrong as this is a mock signer + signature: toBase64(txHash), }, } }, } - } - parseRSVSignature(rsvSignature: RSVSignature): Uint8Array { - return new Uint8Array([ - ...fromHex(rsvSignature.r), - ...fromHex(rsvSignature.s), - ]) - } - - private createFee(denom: string, gasPrice: number, gas?: number): StdFee { - const gasLimit = gas || 200_000 - return calculateFee(gasLimit, GasPrice.fromString(`${gasPrice}${denom}`)) - } + const client = await SigningStargateClient.connectWithSigner( + rpcUrl, + signer, + { + registry: this.registry, + gasPrice: GasPrice.fromString(`${gasPrice}${denom}`), + } + ) - private updateMessages( - messages: EncodeObject[], - address: string - ): EncodeObject[] { - return messages.map((msg) => + const gasLimit = transactionRequest.gas || 200_000 + const fee = calculateFee( + gasLimit, + GasPrice.fromString(`${gasPrice}${denom}`) + ) + const updatedMessages = transactionRequest.messages.map((msg) => !msg.value.fromAddress - ? { ...msg, value: { ...msg.value, fromAddress: address } } + ? { + ...msg, + value: { ...msg.value, fromAddress: transactionRequest.address }, + } : msg ) - } - async handleTransaction( - data: CosmosTransaction, - nearAuthentication: NearAuthentication, - path: KeyDerivationPath - ): Promise { - const { prefix, denom, rpcUrl, gasPrice } = await fetchChainInfo( - this.chainId + await client.sign( + transactionRequest.address, + updatedMessages, + fee, + transactionRequest.memo || '' ) - const { address, publicKey } = await fetchDerivedCosmosAddressAndPublicKey({ - signerId: nearAuthentication.accountId, - path, - nearNetworkId: nearAuthentication.networkId, - multichainContractId: this.contract, - prefix, - }) + return { + transaction: { + address: transactionRequest.address, + publicKey: transactionRequest.publicKey, + messages: updatedMessages, + memo: transactionRequest.memo, + fee, + }, + mpcPayloads: payloads.map((payload, index) => ({ + index, + payload, + })), + } + } + + async addSignatureAndBroadcast({ + transaction, + mpcSignatures, + }: { + transaction: CosmosUnsignedTransaction + mpcSignatures: MPCSignature[] + }): Promise { + const { denom, rpcUrl, gasPrice } = await fetchChainInfo(this.chainId) + const publicKeyBuffer = Buffer.from(transaction.publicKey, 'hex') + + const signer: OfflineDirectSigner = { + getAccounts: async () => [ + { + address: transaction.address, + algo: 'secp256k1', + pubkey: publicKeyBuffer, + }, + ], + signDirect: async (signerAddress: string, signDoc: SignDoc) => { + if (signerAddress !== transaction.address) { + throw new Error(`Address ${signerAddress} not found in wallet`) + } + + // TODO: Should handle multiple signatures + const signature = this.parseRSVSignature(toRSV(mpcSignatures[0])) - const signer = await this.createSigner(address, publicKey) + return { + signed: signDoc, + signature: { + pub_key: { + type: 'tendermint/PubKeySecp256k1', + value: toBase64(publicKeyBuffer), + }, + signature: toBase64(signature), + }, + } + }, + } const client = await SigningStargateClient.connectWithSigner( rpcUrl, @@ -130,14 +278,11 @@ export class Cosmos { } ) - const fee = this.createFee(denom, gasPrice, data.gas) - const updatedMessages = this.updateMessages(data.messages, address) - const result = await client.signAndBroadcast( - address, - updatedMessages, - fee, - data.memo || '' + transaction.address, + transaction.messages, + transaction.fee, + transaction.memo || '' ) assertIsDeliverTxSuccess(result) diff --git a/src/chains/Cosmos/types.ts b/src/chains/Cosmos/types.ts index e4c2b51..9645cc1 100644 --- a/src/chains/Cosmos/types.ts +++ b/src/chains/Cosmos/types.ts @@ -1,29 +1,27 @@ // types.ts -import { type KeyDerivationPath } from '../../kdf/types' -import { - type NearNetworkIds, - type ChainSignatureContracts, - type NearAuthentication, -} from '../types' +import { type StdFee } from '@cosmjs/stargate' +import { type KeyDerivationPath } from '../../signature' +import { type ChainSignatureContracts, type NearAuthentication } from '../types' import { type EncodeObject } from '@cosmjs/proto-signing' export type CosmosNetworkIds = string -export interface CosmosTransaction { - messages: EncodeObject[] // Array of messages (EncodeObject) +export interface CosmosUnsignedTransaction { + address: string + publicKey: string + messages: EncodeObject[] memo?: string - gas?: number + fee: StdFee } -export interface CosmosPublicKeyAndAddressRequest { - signerId: string - path: KeyDerivationPath - nearNetworkId: NearNetworkIds - multichainContractId: ChainSignatureContracts - prefix: string +export interface CosmosTransactionRequest { + address: string + publicKey: string + messages: EncodeObject[] // Array of messages (EncodeObject) + memo?: string + gas?: number } - export interface CosmosChainConfig { contract: ChainSignatureContracts chainId: CosmosNetworkIds @@ -31,8 +29,19 @@ export interface CosmosChainConfig { export interface CosmosRequest { chainConfig: CosmosChainConfig - transaction: CosmosTransaction + transaction: CosmosTransactionRequest nearAuthentication: NearAuthentication derivationPath: KeyDerivationPath fastAuthRelayerUrl?: string } + +export interface BalanceResponse { + balances: Array<{ + denom: string + amount: string + }> + pagination: { + next_key: string | null + total: string + } +} diff --git a/src/chains/Cosmos/utils.ts b/src/chains/Cosmos/utils.ts index b62e0a8..85a1062 100644 --- a/src/chains/Cosmos/utils.ts +++ b/src/chains/Cosmos/utils.ts @@ -1,49 +1,4 @@ -import { fromHex } from '@cosmjs/encoding' -import { Secp256k1, sha256, ripemd160 } from '@cosmjs/crypto' -import { bech32 } from 'bech32' - -import { najToPubKey } from '../../kdf/kdf' -import { getCanonicalizedDerivationPath } from '../../kdf/utils' -import { type CosmosPublicKeyAndAddressRequest } from './types' -import { chains } from 'chain-registry' -import { StargateClient } from '@cosmjs/stargate' -import { ChainSignaturesContract } from '../../signature/chain-signatures-contract' - -export async function fetchDerivedCosmosAddressAndPublicKey({ - signerId, - path, - nearNetworkId, - multichainContractId, - prefix, -}: CosmosPublicKeyAndAddressRequest): Promise<{ - address: string - publicKey: Buffer -}> { - const derivedPubKeyNAJ = await ChainSignaturesContract.getDerivedPublicKey({ - networkId: nearNetworkId, - contract: multichainContractId, - args: { path: getCanonicalizedDerivationPath(path), predecessor: signerId }, - }) - - if (!derivedPubKeyNAJ) { - throw new Error('Failed to get derived public key') - } - - const derivedKey = najToPubKey(derivedPubKeyNAJ, { compress: true }) - const publicKey = fromHex(derivedKey) - const address = pubkeyToAddress(publicKey, prefix) - - return { address, publicKey: Buffer.from(publicKey) } -} - -function pubkeyToAddress(pubkey: Uint8Array, prefix: string): string { - const pubkeyRaw = - pubkey.length === 33 ? pubkey : Secp256k1.compressPubkey(pubkey) - const sha256Hash = sha256(pubkeyRaw) - const ripemd160Hash = ripemd160(sha256Hash) - const address = bech32.encode(prefix, bech32.toWords(ripemd160Hash)) - return address -} +import { chains, assets } from 'chain-registry' export const fetchChainInfo = async ( chainId: string @@ -54,6 +9,7 @@ export const fetchChainInfo = async ( restUrl: string expectedChainId: string gasPrice: number + decimals: number }> => { const chainInfo = chains.find((chain) => chain.chain_id === chainId) if (!chainInfo) { @@ -79,22 +35,19 @@ export const fetchChainInfo = async ( ) } - return { prefix, denom, rpcUrl, restUrl, expectedChainId, gasPrice } -} - -export async function fetchCosmosBalance( - address: string, - chainId: string -): Promise { - try { - const { restUrl, denom } = await fetchChainInfo(chainId) - const client = await StargateClient.connect(restUrl) - - const balance = await client.getBalance(address, denom) + const assetList = assets.find( + (asset) => asset.chain_name === chainInfo.chain_name + ) + const asset = assetList?.assets.find((asset) => asset.base === denom) + const decimals = asset?.denom_units.find( + (unit) => unit.denom === asset.display + )?.exponent - return balance.amount - } catch (error) { - console.error('Failed to fetch Cosmos balance:', error) - throw new Error('Failed to fetch Cosmos balance') + if (decimals === undefined) { + throw new Error( + `Could not find decimals for ${denom} on chain ${chainInfo.chain_name}` + ) } + + return { prefix, denom, rpcUrl, restUrl, expectedChainId, gasPrice, decimals } } diff --git a/src/chains/EVM/EVM.ts b/src/chains/EVM/EVM.ts index 7e4e66d..d377d30 100644 --- a/src/chains/EVM/EVM.ts +++ b/src/chains/EVM/EVM.ts @@ -1,68 +1,47 @@ import { ethers, keccak256 } from 'ethers' - -import { fetchDerivedEVMAddress, fetchEVMFeeProperties } from './utils' -import { type ChainSignatureContracts, type NearAuthentication } from '../types' -import { type EVMTransaction } from './types' -import { type KeyDerivationPath } from '../../kdf/types' -import { toRSV } from '../../signature/utils' -import { type MPCSignature, type RSVSignature } from '../../signature/types' - -export class EVM { +import { fetchEVMFeeProperties } from './utils' +import { + type MPCPayloads, + type ChainSignatureContracts, + type NearNetworkIds, +} from '../types' +import { + type EVMTransactionRequest, + type EVMUnsignedTransaction, +} from './types' +import { toRSV, najToPubKey } from '../../signature/utils' +import { + type RSVSignature, + type MPCSignature, + type KeyDerivationPath, +} from '../../signature/types' +import { ChainSignaturesContract } from '../../contracts' +import { type Chain } from '../Chain' + +export class EVM + implements Chain +{ private readonly provider: ethers.JsonRpcProvider private readonly contract: ChainSignatureContracts - private readonly signer: (txHash: Uint8Array) => Promise + private readonly nearNetworkId: NearNetworkIds constructor(config: { providerUrl: string contract: ChainSignatureContracts - signer: (txHash: Uint8Array) => Promise + nearNetworkId: NearNetworkIds }) { this.provider = new ethers.JsonRpcProvider(config.providerUrl) this.contract = config.contract - this.signer = config.signer - } - - static prepareTransactionForSignature( - transaction: ethers.TransactionLike - ): Uint8Array { - const serializedTransaction = - ethers.Transaction.from(transaction).unsignedSerialized - const transactionHash = keccak256(serializedTransaction) - - return new Uint8Array(ethers.getBytes(transactionHash)) - } - - async sendSignedTransaction( - transaction: ethers.TransactionLike, - signature: ethers.SignatureLike - ): Promise { - try { - const serializedTransaction = ethers.Transaction.from({ - ...transaction, - signature, - }).serialized - return await this.provider.broadcastTransaction(serializedTransaction) - } catch (error) { - console.error('Transaction execution failed:', error) - throw new Error('Failed to send signed transaction.') - } + this.nearNetworkId = config.nearNetworkId } - async attachGasAndNonce( - transaction: Omit & { from: string } - ): Promise { - const hasUserProvidedGas = - transaction.gasLimit && - transaction.maxFeePerGas && - transaction.maxPriorityFeePerGas - - const { gasLimit, maxFeePerGas, maxPriorityFeePerGas } = hasUserProvidedGas - ? transaction - : await fetchEVMFeeProperties( - this.provider._getConnection().url, - transaction - ) - + private async attachGasAndNonce( + transaction: EVMTransactionRequest + ): Promise { + const fees = await fetchEVMFeeProperties( + this.provider._getConnection().url, + transaction + ) const nonce = await this.provider.getTransactionCount( transaction.from, 'latest' @@ -71,9 +50,7 @@ export class EVM { const { from, ...rest } = transaction return { - gasLimit, - maxFeePerGas, - maxPriorityFeePerGas, + ...fees, chainId: this.provider._network.chainId, nonce, type: 2, @@ -81,6 +58,46 @@ export class EVM { } } + private parseSignature(signature: RSVSignature): ethers.SignatureLike { + return ethers.Signature.from({ + r: `0x${signature.r}`, + s: `0x${signature.s}`, + v: signature.v, + }) + } + + // TODO: Should accept a derivedPubKeyNAJ as an argument so we can remove the contract dependency + async deriveAddressAndPublicKey( + signerId: string, + path: KeyDerivationPath + ): Promise<{ + address: string + publicKey: string + }> { + const derivedPubKeyNAJ = await ChainSignaturesContract.getDerivedPublicKey({ + networkId: this.nearNetworkId, + contract: this.contract, + args: { path, predecessor: signerId }, + }) + + if (!derivedPubKeyNAJ) { + throw new Error('Failed to get derived public key') + } + + const childPublicKey = najToPubKey(derivedPubKeyNAJ, { compress: false }) + + const publicKeyNoPrefix = childPublicKey.startsWith('04') + ? childPublicKey.substring(2) + : childPublicKey + + const hash = ethers.keccak256(Buffer.from(publicKeyNoPrefix, 'hex')) + + return { + address: `0x${hash.substring(hash.length - 40)}`, + publicKey: childPublicKey, + } + } + async getBalance(address: string): Promise { try { const balance = await this.provider.getBalance(address) @@ -91,46 +108,69 @@ export class EVM { } } - parseRSVSignature(rsvSignature: RSVSignature): ethers.Signature { - const r = `0x${rsvSignature.r}` - const s = `0x${rsvSignature.s}` - const v = rsvSignature.v - - return ethers.Signature.from({ r, s, v }) + setTransaction( + transaction: EVMUnsignedTransaction, + storageKey: string + ): void { + const serializedTransaction = JSON.stringify(transaction, (_, value) => + typeof value === 'bigint' ? value.toString() : value + ) + window.localStorage.setItem(storageKey, serializedTransaction) } - async handleTransaction( - data: EVMTransaction, - nearAuthentication: NearAuthentication, - path: KeyDerivationPath - ): Promise { - const derivedFrom = await fetchDerivedEVMAddress({ - signerId: nearAuthentication.accountId, - path, - nearNetworkId: nearAuthentication.networkId, - multichainContractId: this.contract, - }) - - if (data.from && data.from.toLowerCase() !== derivedFrom.toLowerCase()) { - throw new Error( - 'Provided "from" address does not match the derived address' - ) + getTransaction( + storageKey: string, + options?: { + remove?: boolean } + ): EVMUnsignedTransaction | undefined { + const txSerialized = window.localStorage.getItem(storageKey) + if (options?.remove) { + window.localStorage.removeItem(storageKey) + } + return txSerialized ? JSON.parse(txSerialized) : undefined + } - const from = data.from || derivedFrom - - const transaction = await this.attachGasAndNonce({ - ...data, - from, - }) + async getMPCPayloadAndTransaction( + transactionRequest: EVMTransactionRequest + ): Promise<{ + transaction: EVMUnsignedTransaction + mpcPayloads: MPCPayloads + }> { + const transaction = await this.attachGasAndNonce(transactionRequest) + const txSerialized = ethers.Transaction.from(transaction).unsignedSerialized + const transactionHash = keccak256(txSerialized) + const txHash = new Uint8Array(ethers.getBytes(transactionHash)) - const txHash = EVM.prepareTransactionForSignature(transaction) - const mpcSignature = await this.signer(txHash) - const transactionResponse = await this.sendSignedTransaction( + return { transaction, - this.parseRSVSignature(toRSV(mpcSignature)) - ) + mpcPayloads: [ + { + index: 0, + payload: txHash, + }, + ], + } + } - return transactionResponse + async addSignatureAndBroadcast({ + transaction, + mpcSignatures, + }: { + transaction: EVMUnsignedTransaction + mpcSignatures: MPCSignature[] + }): Promise { + try { + const txSerialized = ethers.Transaction.from({ + ...transaction, + signature: this.parseSignature(toRSV(mpcSignatures[0])), + }).serialized + const txResponse = await this.provider.broadcastTransaction(txSerialized) + + return txResponse.hash + } catch (error) { + console.error('Transaction execution failed:', error) + throw new Error('Failed to send signed transaction.') + } } } diff --git a/src/chains/EVM/types.ts b/src/chains/EVM/types.ts index 6553f09..5d4c566 100644 --- a/src/chains/EVM/types.ts +++ b/src/chains/EVM/types.ts @@ -1,26 +1,19 @@ import type * as ethers from 'ethers' -import { - type ChainSignatureContracts, - type NearNetworkIds, - type ChainProvider, - type NearAuthentication, -} from '../types' -import { type KeyDerivationPath } from '../../kdf/types' +import { type ChainProvider, type NearAuthentication } from '../types' +import { type KeyDerivationPath } from '../../signature' -export type EVMTransaction = ethers.TransactionLike +export type EVMUnsignedTransaction = ethers.TransactionLike + +export type EVMTransactionRequest = Omit & { + from: string +} export type EVMChainConfigWithProviders = ChainProvider export interface EVMRequest { - transaction: EVMTransaction + transaction: EVMTransactionRequest chainConfig: EVMChainConfigWithProviders nearAuthentication: NearAuthentication fastAuthRelayerUrl?: string derivationPath: KeyDerivationPath } -export interface FetchEVMAddressRequest { - signerId: string - path: KeyDerivationPath - nearNetworkId: NearNetworkIds - multichainContractId: ChainSignatureContracts -} diff --git a/src/chains/EVM/utils.ts b/src/chains/EVM/utils.ts index 774c412..6af811d 100644 --- a/src/chains/EVM/utils.ts +++ b/src/chains/EVM/utils.ts @@ -1,10 +1,5 @@ import { ethers } from 'ethers' -import { ChainSignaturesContract } from '../../signature' -import { getCanonicalizedDerivationPath } from '../../kdf/utils' -import { type FetchEVMAddressRequest } from './types' -import { najToPubKey } from '../../kdf/kdf' - export async function fetchEVMFeeProperties( providerUrl: string, transaction: ethers.TransactionLike @@ -29,30 +24,3 @@ export async function fetchEVMFeeProperties( maxFee: maxFeePerGas * gasLimit, } } - -export async function fetchDerivedEVMAddress({ - signerId, - path, - nearNetworkId, - multichainContractId, -}: FetchEVMAddressRequest): Promise { - const derivedPubKeyNAJ = await ChainSignaturesContract.getDerivedPublicKey({ - networkId: nearNetworkId, - contract: multichainContractId, - args: { path: getCanonicalizedDerivationPath(path), predecessor: signerId }, - }) - - if (!derivedPubKeyNAJ) { - throw new Error('Failed to get derived public key') - } - - const childPublicKey = najToPubKey(derivedPubKeyNAJ, { compress: false }) - - const publicKeyNoPrefix = childPublicKey.startsWith('04') - ? childPublicKey.substring(2) - : childPublicKey - - const hash = ethers.keccak256(Buffer.from(publicKeyNoPrefix, 'hex')) - - return `0x${hash.substring(hash.length - 40)}` -} diff --git a/src/chains/types.ts b/src/chains/types.ts index 19844e6..d946a24 100644 --- a/src/chains/types.ts +++ b/src/chains/types.ts @@ -1,6 +1,3 @@ -import { type KeyPair } from '@near-js/crypto' -import type BN from 'bn.js' - /** Available ChainSignature contracts: - Mainnet: v1.signer @@ -9,6 +6,8 @@ Available ChainSignature contracts: */ export type ChainSignatureContracts = string +export type NFTKeysContracts = string + export interface ChainProvider { providerUrl: string contract: ChainSignatureContracts @@ -16,9 +15,7 @@ export interface ChainProvider { export interface NearAuthentication { networkId: NearNetworkIds - keypair: KeyPair accountId: string - deposit?: BN } interface SuccessResponse { @@ -34,3 +31,5 @@ interface FailureResponse { export type Response = SuccessResponse | FailureResponse export type NearNetworkIds = 'mainnet' | 'testnet' + +export type MPCPayloads = Array<{ index: number; payload: Uint8Array }> diff --git a/src/signature/chain-signatures-contract.ts b/src/contracts/ChainSignaturesContract/ChainSignaturesContract.ts similarity index 81% rename from src/signature/chain-signatures-contract.ts rename to src/contracts/ChainSignaturesContract/ChainSignaturesContract.ts index 320f6ce..99116d4 100644 --- a/src/signature/chain-signatures-contract.ts +++ b/src/contracts/ChainSignaturesContract/ChainSignaturesContract.ts @@ -1,19 +1,23 @@ import { type Account, Contract } from '@near-js/accounts' import { actionCreators } from '@near-js/transactions' -import { getNearAccount, NEAR_MAX_GAS } from './utils' + import BN from 'bn.js' import { ethers } from 'ethers' -import { type MPCSignature } from './types' +import { + type MPCSignature, + type KeyDerivationPath, +} from '../../signature/types' import { type NearNetworkIds, type ChainSignatureContracts, type NearAuthentication, -} from '../chains/types' -import { parseSignedDelegateForRelayer } from '../relayer' -import { type ExecutionOutcomeWithId } from 'near-api-js/lib/providers' -import { type KeyDerivationPath } from '../kdf/types' -import { getCanonicalizedDerivationPath } from '../kdf/utils' +} from '../../chains/types' +import { parseSignedDelegateForRelayer } from '../../relayer' +import { type KeyPair } from '@near-js/crypto' +import { NEAR_MAX_GAS } from '../../signature/utils' +import { getNearAccount } from '../utils' +import { transactionBuilder } from '../..' interface SignArgs { payload: number[] @@ -111,27 +115,31 @@ export const ChainSignaturesContract = { nearAuthentication, contract, relayerUrl, + keypair, + proposedDeposit, }: { hashedTx: Uint8Array path: KeyDerivationPath nearAuthentication: NearAuthentication contract: ChainSignatureContracts relayerUrl?: string + keypair: KeyPair + proposedDeposit?: BN }): Promise => { const account = await getNearAccount({ networkId: nearAuthentication.networkId, accountId: nearAuthentication.accountId, - keypair: nearAuthentication.keypair, + keypair, }) const mpcPayload = { payload: Array.from(ethers.getBytes(hashedTx)), - path: getCanonicalizedDerivationPath(path), + path, key_version: 0, } const deposit = - nearAuthentication.deposit ?? + proposedDeposit ?? (await ChainSignaturesContract.getCurrentFee({ networkId: nearAuthentication.networkId, contract, @@ -217,8 +225,6 @@ const signWithRelayer = async ({ signedDelegate.delegateAction.publicKey.toString() ] - // TODO: add support for creating the signed delegate using the mpc recovery service with an oidc_token - const res = await fetch(`${relayerUrl}/send_meta_tx_async`, { method: 'POST', mode: 'cors', @@ -233,27 +239,13 @@ const signWithRelayer = async ({ 'FINAL' ) - const signature: string = txStatus.receipts_outcome.reduce( - (acc: string, curr: ExecutionOutcomeWithId) => { - if (acc) { - return acc - } - const { status } = curr.outcome - return ( - (typeof status === 'object' && - status.SuccessValue && - status.SuccessValue !== '' && - Buffer.from(status.SuccessValue, 'base64').toString('utf-8')) || - '' - ) - }, - '' - ) - if (signature) { - const parsedJSONSignature = JSON.parse(signature) as { - Ok: MPCSignature - } - return parsedJSONSignature.Ok + const signature = transactionBuilder.near.responseToMpcSignature({ + response: txStatus, + }) + + if (!signature) { + throw new Error('Signature error, please retry') } - throw new Error('Signature error, please retry') + + return signature } diff --git a/src/contracts/ChainSignaturesContract/index.ts b/src/contracts/ChainSignaturesContract/index.ts new file mode 100644 index 0000000..e7ae8e0 --- /dev/null +++ b/src/contracts/ChainSignaturesContract/index.ts @@ -0,0 +1 @@ +export * from './ChainSignaturesContract' diff --git a/src/contracts/index.ts b/src/contracts/index.ts new file mode 100644 index 0000000..e7ae8e0 --- /dev/null +++ b/src/contracts/index.ts @@ -0,0 +1 @@ +export * from './ChainSignaturesContract' diff --git a/src/contracts/utils.ts b/src/contracts/utils.ts new file mode 100644 index 0000000..4cefd65 --- /dev/null +++ b/src/contracts/utils.ts @@ -0,0 +1,40 @@ +import { Account, Connection } from '@near-js/accounts' +import { InMemoryKeyStore } from '@near-js/keystores' +import { KeyPair } from '@near-js/crypto' + +type SetConnectionArgs = + | { + networkId: string + accountId: string + keypair: KeyPair + } + | { + networkId: string + accountId?: never + keypair?: never + } + +export const getNearAccount = async ({ + networkId, + accountId = 'dontcare', + keypair = KeyPair.fromRandom('ed25519'), +}: SetConnectionArgs): Promise => { + const keyStore = new InMemoryKeyStore() + await keyStore.setKey(networkId, accountId, keypair) + + const connection = Connection.fromConfig({ + networkId, + provider: { + type: 'JsonRpcProvider', + args: { + url: { + testnet: 'https://rpc.testnet.near.org', + mainnet: 'https://rpc.mainnet.near.org', + }[networkId], + }, + }, + signer: { type: 'InMemorySigner', keyStore }, + }) + + return new Account(connection, accountId) +} diff --git a/src/index.ts b/src/index.ts index ba04567..972fd7e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,48 +1,42 @@ export type { NearNetworkIds, ChainSignatureContracts } from './chains/types' -export type { SLIP044ChainId, KeyDerivationPath } from './kdf/types' -export { ChainSignaturesContract } from './signature/chain-signatures-contract' -export * from './signAndSendMethods' +export type { SLIP044ChainId, KeyDerivationPath } from './signature/types' +export { ChainSignaturesContract } from './contracts' +export * as signAndSend from './sign-and-send-methods' +export * as transactionBuilder from './transaction-builder' +export type { Chain } from './chains/Chain' // EVM export { EVM } from './chains/EVM/EVM' -export { - fetchDerivedEVMAddress, - fetchEVMFeeProperties, -} from './chains/EVM/utils' +export { fetchEVMFeeProperties } from './chains/EVM/utils' export type { - FetchEVMAddressRequest, EVMChainConfigWithProviders, EVMRequest, + EVMTransactionRequest, + EVMUnsignedTransaction, } from './chains/EVM/types' // Bitcoin export { Bitcoin } from './chains/Bitcoin/Bitcoin' -export { - fetchBTCFeeProperties, - fetchDerivedBTCAddressAndPublicKey, -} from './chains/Bitcoin/utils' +export { fetchBTCFeeProperties } from './chains/Bitcoin/utils' export type { - BitcoinPublicKeyAndAddressRequest, + BTCChainConfigWithProviders, BTCNetworkIds, BitcoinRequest, - BTCChainConfigWithProviders, + BTCTransactionRequest, + BTCUnsignedTransaction, } from './chains/Bitcoin/types' // Cosmos export { Cosmos } from './chains/Cosmos/Cosmos' -export { - fetchDerivedCosmosAddressAndPublicKey, - fetchCosmosBalance, -} from './chains/Cosmos/utils' - export type { - CosmosPublicKeyAndAddressRequest, + CosmosChainConfig, CosmosNetworkIds, CosmosRequest, - CosmosChainConfig, + CosmosTransactionRequest, + CosmosUnsignedTransaction, } from './chains/Cosmos/types' diff --git a/src/kdf/kdf.ts b/src/kdf/kdf.ts deleted file mode 100644 index d267a78..0000000 --- a/src/kdf/kdf.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { base_decode } from 'near-api-js/lib/utils/serialize' - -export const najToPubKey = ( - najPubKey: string, - options: { - compress: boolean - } -): string => { - const uncompressedPubKey = `04${Buffer.from(base_decode(najPubKey.split(':')[1])).toString('hex')}` - - if (!options.compress) { - return uncompressedPubKey - } - - const pubKeyHex = uncompressedPubKey.startsWith('04') - ? uncompressedPubKey.slice(2) - : uncompressedPubKey - - if (pubKeyHex.length !== 128) { - throw new Error('Invalid uncompressed public key length') - } - - const x = pubKeyHex.slice(0, 64) - const y = pubKeyHex.slice(64) - - const isEven = parseInt(y.slice(-1), 16) % 2 === 0 - const prefix = isEven ? '02' : '03' - - return prefix + x -} diff --git a/src/kdf/types.ts b/src/kdf/types.ts deleted file mode 100644 index aaba959..0000000 --- a/src/kdf/types.ts +++ /dev/null @@ -1,7 +0,0 @@ -export type SLIP044ChainId = 0 | 60 | 118 - -export interface KeyDerivationPath { - chain: SLIP044ChainId - domain?: string - meta?: Record -} diff --git a/src/kdf/utils.ts b/src/kdf/utils.ts deleted file mode 100644 index 9b74376..0000000 --- a/src/kdf/utils.ts +++ /dev/null @@ -1,17 +0,0 @@ -import canonicalize from 'canonicalize' -import { type KeyDerivationPath } from './types' -import pickBy from 'lodash.pickby' - -export const getCanonicalizedDerivationPath = ( - derivationPath: KeyDerivationPath -): string => - canonicalize( - pickBy( - { - chain: derivationPath.chain, - domain: derivationPath.domain, - meta: derivationPath.meta, - }, - (v: any) => v !== undefined && v !== null - ) - ) ?? '' diff --git a/src/sign-and-send-methods/index.ts b/src/sign-and-send-methods/index.ts new file mode 100644 index 0000000..e5f0ad7 --- /dev/null +++ b/src/sign-and-send-methods/index.ts @@ -0,0 +1 @@ +export * as keyPair from './keypair' diff --git a/src/sign-and-send-methods/keypair.ts b/src/sign-and-send-methods/keypair.ts new file mode 100644 index 0000000..d77c30e --- /dev/null +++ b/src/sign-and-send-methods/keypair.ts @@ -0,0 +1,144 @@ +import { Bitcoin } from '../chains/Bitcoin/Bitcoin' +import { type BitcoinRequest } from '../chains/Bitcoin/types' +import { type CosmosRequest } from '../chains/Cosmos/types' +import { Cosmos } from '../chains/Cosmos/Cosmos' +import { EVM } from '../chains/EVM/EVM' +import { type EVMRequest } from '../chains/EVM/types' +import { type Response } from '../chains/types' +import { ChainSignaturesContract } from '../contracts' +import { type KeyPair } from '@near-js/crypto' + +export const signAndSendEVMTransaction = async ( + req: EVMRequest, + keyPair: KeyPair +): Promise => { + try { + const evm = new EVM({ + providerUrl: req.chainConfig.providerUrl, + contract: req.chainConfig.contract, + nearNetworkId: req.nearAuthentication.networkId, + }) + + const { transaction, mpcPayloads } = await evm.getMPCPayloadAndTransaction( + req.transaction + ) + + const signature = await ChainSignaturesContract.sign({ + hashedTx: mpcPayloads[0].payload, + path: req.derivationPath, + nearAuthentication: req.nearAuthentication, + contract: req.chainConfig.contract, + relayerUrl: req.fastAuthRelayerUrl, + keypair: keyPair, + }) + + const txHash = await evm.addSignatureAndBroadcast({ + transaction, + mpcSignatures: [signature], + }) + + return { + transactionHash: txHash, + success: true, + } + } catch (e: unknown) { + console.error(e) + return { + success: false, + errorMessage: e instanceof Error ? e.message : String(e), + } + } +} + +export const signAndSendBTCTransaction = async ( + req: BitcoinRequest, + keyPair: KeyPair +): Promise => { + try { + const btc = new Bitcoin({ + providerUrl: req.chainConfig.providerUrl, + contract: req.chainConfig.contract, + network: req.chainConfig.network, + nearNetworkId: req.nearAuthentication.networkId, + }) + + const { transaction, mpcPayloads } = await btc.getMPCPayloadAndTransaction( + req.transaction + ) + + const signatures = await Promise.all( + mpcPayloads.map( + async ({ payload }) => + await ChainSignaturesContract.sign({ + hashedTx: payload, + path: req.derivationPath, + nearAuthentication: req.nearAuthentication, + contract: req.chainConfig.contract, + relayerUrl: req.fastAuthRelayerUrl, + keypair: keyPair, + }) + ) + ) + + const txHash = await btc.addSignatureAndBroadcast({ + transaction, + mpcSignatures: signatures, + }) + + return { + transactionHash: txHash, + success: true, + } + } catch (e: unknown) { + return { + success: false, + errorMessage: e instanceof Error ? e.message : String(e), + } + } +} + +export const signAndSendCosmosTransaction = async ( + req: CosmosRequest, + keyPair: KeyPair +): Promise => { + try { + const cosmos = new Cosmos({ + contract: req.chainConfig.contract, + chainId: req.chainConfig.chainId, + nearNetworkId: req.nearAuthentication.networkId, + }) + + const { transaction, mpcPayloads } = + await cosmos.getMPCPayloadAndTransaction(req.transaction) + + const signatures = await Promise.all( + mpcPayloads.map( + async ({ payload }) => + await ChainSignaturesContract.sign({ + hashedTx: payload, + path: req.derivationPath, + nearAuthentication: req.nearAuthentication, + contract: req.chainConfig.contract, + relayerUrl: req.fastAuthRelayerUrl, + keypair: keyPair, + }) + ) + ) + + const txHash = await cosmos.addSignatureAndBroadcast({ + transaction, + mpcSignatures: signatures, + }) + + return { + transactionHash: txHash, + success: true, + } + } catch (e: unknown) { + console.error(e) + return { + success: false, + errorMessage: e instanceof Error ? e.message : String(e), + } + } +} diff --git a/src/signAndSendMethods.ts b/src/signAndSendMethods.ts deleted file mode 100644 index edc57d2..0000000 --- a/src/signAndSendMethods.ts +++ /dev/null @@ -1,121 +0,0 @@ -import { Bitcoin } from './chains/Bitcoin/Bitcoin' -import { type BitcoinRequest } from './chains/Bitcoin/types' -import { type CosmosRequest } from './chains/Cosmos/types' -import { Cosmos } from './chains/Cosmos/Cosmos' -import { EVM } from './chains/EVM/EVM' -import { type EVMRequest } from './chains/EVM/types' -import { type Response } from './chains/types' -import { ChainSignaturesContract } from './signature/chain-signatures-contract' - -export const signAndSendEVMTransaction = async ( - req: EVMRequest -): Promise => { - try { - const evm = new EVM({ - ...req.chainConfig, - signer: async (txHash) => - await ChainSignaturesContract.sign({ - hashedTx: txHash, - path: req.derivationPath, - nearAuthentication: req.nearAuthentication, - contract: req.chainConfig.contract, - relayerUrl: req.fastAuthRelayerUrl, - }), - }) - - const res = await evm.handleTransaction( - req.transaction, - req.nearAuthentication, - req.derivationPath - ) - - if (res) { - return { - transactionHash: res.hash, - success: true, - } - } else { - console.error(res) - return { - success: false, - errorMessage: 'Transaction failed', - } - } - } catch (e: unknown) { - console.error(e) - return { - success: false, - errorMessage: e instanceof Error ? e.message : String(e), - } - } -} - -export const signAndSendBTCTransaction = async ( - req: BitcoinRequest -): Promise => { - try { - const btc = new Bitcoin({ - ...req.chainConfig, - signer: async (txHash) => - await ChainSignaturesContract.sign({ - hashedTx: txHash, - path: req.derivationPath, - nearAuthentication: req.nearAuthentication, - contract: req.chainConfig.contract, - relayerUrl: req.fastAuthRelayerUrl, - }), - }) - - const txid = await btc.handleTransaction( - req.transaction, - req.nearAuthentication, - req.derivationPath - ) - - return { - transactionHash: txid, - success: true, - } - } catch (e: unknown) { - return { - success: false, - errorMessage: e instanceof Error ? e.message : String(e), - } - } -} - -export const signAndSendCosmosTransaction = async ( - req: CosmosRequest -): Promise => { - try { - const cosmos = new Cosmos({ - contract: req.chainConfig.contract, - chainId: req.chainConfig.chainId, - signer: async (txHash) => - await ChainSignaturesContract.sign({ - hashedTx: txHash, - path: req.derivationPath, - nearAuthentication: req.nearAuthentication, - contract: req.chainConfig.contract, - relayerUrl: req.fastAuthRelayerUrl, - }), - }) - - const txHash = await cosmos.handleTransaction( - req.transaction, - req.nearAuthentication, - req.derivationPath - ) - - return { - transactionHash: txHash, - success: true, - } - } catch (e: unknown) { - console.error(e) - return { - success: false, - errorMessage: e instanceof Error ? e.message : String(e), - } - } -} diff --git a/src/signature/index.ts b/src/signature/index.ts index 64e20d2..ce4acb5 100644 --- a/src/signature/index.ts +++ b/src/signature/index.ts @@ -1 +1,2 @@ -export * from './chain-signatures-contract' +export * from './types' +export * from './utils' diff --git a/src/signature/types.ts b/src/signature/types.ts index b7d6c22..b4ccabc 100644 --- a/src/signature/types.ts +++ b/src/signature/types.ts @@ -1,3 +1,5 @@ +export type SLIP044ChainId = 0 | 60 | 118 +export type KeyDerivationPath = string export interface RSVSignature { r: string s: string diff --git a/src/signature/utils.ts b/src/signature/utils.ts index 64ac1c5..fee8b83 100644 --- a/src/signature/utils.ts +++ b/src/signature/utils.ts @@ -1,12 +1,38 @@ import { type MPCSignature, type RSVSignature } from './types' import BN from 'bn.js' - -import { Account, Connection } from '@near-js/accounts' -import { InMemoryKeyStore } from '@near-js/keystores' -import { KeyPair } from '@near-js/crypto' +import { base_decode } from 'near-api-js/lib/utils/serialize' export const NEAR_MAX_GAS = new BN('300000000000000') +export const najToPubKey = ( + najPubKey: string, + options: { + compress: boolean + } +): string => { + const uncompressedPubKey = `04${Buffer.from(base_decode(najPubKey.split(':')[1])).toString('hex')}` + + if (!options.compress) { + return uncompressedPubKey + } + + const pubKeyHex = uncompressedPubKey.startsWith('04') + ? uncompressedPubKey.slice(2) + : uncompressedPubKey + + if (pubKeyHex.length !== 128) { + throw new Error('Invalid uncompressed public key length') + } + + const x = pubKeyHex.slice(0, 64) + const y = pubKeyHex.slice(64) + + const isEven = parseInt(y.slice(-1), 16) % 2 === 0 + const prefix = isEven ? '02' : '03' + + return prefix + x +} + export const toRSV = (signature: MPCSignature): RSVSignature => { return { r: signature.big_r.affine_point.substring(2), @@ -14,40 +40,3 @@ export const toRSV = (signature: MPCSignature): RSVSignature => { v: signature.recovery_id, } } - -type SetConnectionArgs = - | { - networkId: string - accountId: string - keypair: KeyPair - } - | { - networkId: string - accountId?: never - keypair?: never - } - -export const getNearAccount = async ({ - networkId, - accountId = 'dontcare', - keypair = KeyPair.fromRandom('ed25519'), -}: SetConnectionArgs): Promise => { - const keyStore = new InMemoryKeyStore() - await keyStore.setKey(networkId, accountId, keypair) - - const connection = Connection.fromConfig({ - networkId, - provider: { - type: 'JsonRpcProvider', - args: { - url: { - testnet: 'https://rpc.testnet.near.org', - mainnet: 'https://rpc.mainnet.near.org', - }[networkId], - }, - }, - signer: { type: 'InMemorySigner', keyStore }, - }) - - return new Account(connection, accountId) -} diff --git a/src/transaction-builder/index.ts b/src/transaction-builder/index.ts new file mode 100644 index 0000000..142eb55 --- /dev/null +++ b/src/transaction-builder/index.ts @@ -0,0 +1 @@ +export * as near from './near' diff --git a/src/transaction-builder/near.ts b/src/transaction-builder/near.ts new file mode 100644 index 0000000..303e6df --- /dev/null +++ b/src/transaction-builder/near.ts @@ -0,0 +1,127 @@ +import type { + Action, + FinalExecutionOutcome, + NetworkId, +} from '@near-wallet-selector/core' +import { + type MPCPayloads, + type ChainSignatureContracts, + type NFTKeysContracts, +} from '../chains/types' +import { type KeyDerivationPath, type MPCSignature } from '../signature/types' +import { ChainSignaturesContract } from '../contracts' +import { type ExecutionOutcomeWithId } from 'near-api-js/lib/providers' +import { NEAR_MAX_GAS } from '../signature/utils' + +export const mpcPayloadsToChainSigTransaction = async ({ + networkId, + contractId, + mpcPayloads, + path, +}: { + networkId: NetworkId + contractId: ChainSignatureContracts + mpcPayloads: MPCPayloads + path: KeyDerivationPath +}): Promise<{ + receiverId: string + actions: Action[] +}> => { + const currentContractFee = await ChainSignaturesContract.getCurrentFee({ + networkId, + contract: contractId, + }) + + return { + receiverId: contractId, + actions: mpcPayloads.map(({ payload }) => ({ + type: 'FunctionCall', + params: { + methodName: 'sign', + args: { + request: { + payload: Array.from(payload), + path, + key_version: 0, + }, + }, + gas: NEAR_MAX_GAS.toString(), + deposit: currentContractFee?.toString() || '1', + }, + })), + } +} + +export const mpcPayloadsToNFTKeysTransaction = async ({ + networkId, + chainSigContract, + nftKeysContract, + mpcPayloads, + path, + tokenId, +}: { + networkId: NetworkId + chainSigContract: ChainSignatureContracts + nftKeysContract: NFTKeysContracts + mpcPayloads: MPCPayloads + path: KeyDerivationPath + tokenId: string +}): Promise<{ + receiverId: string + actions: Action[] +}> => { + const currentContractFee = await ChainSignaturesContract.getCurrentFee({ + networkId, + contract: chainSigContract, + }) + + return { + receiverId: nftKeysContract, + actions: mpcPayloads.map(({ payload }) => ({ + type: 'FunctionCall', + params: { + methodName: 'ckt_sign_hash', + args: { + token_id: tokenId, + path, + payload: Array.from(payload), + }, + gas: NEAR_MAX_GAS.toString(), + deposit: currentContractFee?.toString() || '1', + }, + })), + } +} + +export const responseToMpcSignature = ({ + response, +}: { + response: FinalExecutionOutcome +}): MPCSignature | undefined => { + const signature: string = response.receipts_outcome.reduce( + (acc: string, curr: ExecutionOutcomeWithId) => { + if (acc) { + return acc + } + const { status } = curr.outcome + return ( + (typeof status === 'object' && + status.SuccessValue && + status.SuccessValue !== '' && + Buffer.from(status.SuccessValue, 'base64').toString('utf-8')) || + '' + ) + }, + '' + ) + + if (signature) { + const parsedJSONSignature = JSON.parse(signature) as { + Ok: MPCSignature + } + + return parsedJSONSignature.Ok + } else { + return undefined + } +}