diff --git a/docs/docs/build/bob-sdk/gateway.md b/docs/docs/build/bob-sdk/gateway.md index 1dde5069..426878d6 100644 --- a/docs/docs/build/bob-sdk/gateway.md +++ b/docs/docs/build/bob-sdk/gateway.md @@ -42,7 +42,7 @@ Import the `GatewaySDK` class from `@gobob/bob-sdk` and create an instance of it ```ts title="/src/utils/gateway.ts" import { GatewayQuoteParams, GatewaySDK } from "@gobob/bob-sdk"; -const gatewaySDK = new GatewaySDK("bob"); // or "mainnet" +const gatewaySDK = new GatewaySDK("bob"); // or "bob-sepolia" ``` ### Get Available Tokens @@ -50,7 +50,7 @@ const gatewaySDK = new GatewaySDK("bob"); // or "mainnet" Returns an array of available output tokens for you to offer the user. Typically rendered as a drop-down menu. See [our SDK's source code](https://github.com/bob-collective/bob/blob/9c52341033af1ccbe388e64ef97a23bf6c07ccc7/sdk/src/gateway/tokens.ts#L8) for type information. ```ts -const outputTokensWithInfo = await gatewaySDK.getTokensInfo(); +const outputTokens = await gatewaySDK.getTokens(); ``` ### Get a Quote @@ -63,11 +63,12 @@ We recommend rendering `quote.fee` and [its other fields](https://github.com/bob ```ts const quoteParams: GatewayQuoteParams = { + fromToken: "BTC", fromChain: "Bitcoin", fromUserAddress: "bc1qafk4yhqvj4wep57m62dgrmutldusqde8adh20d", toChain: "BOB", toUserAddress: "0x2D2E86236a5bC1c8a5e5499C517E17Fb88Dbc18c", - toToken: "tBTC", + toToken: "tBTC", // or e.g. "SolvBTC" amount: 10000000, // 0.1 BTC gasRefill: 10000, // 0.0001 BTC. The amount of BTC to swap for ETH for tx fees. }; @@ -75,6 +76,21 @@ const quoteParams: GatewayQuoteParams = { const quote = await gatewaySDK.getQuote(quoteParams); ``` +#### Get available staking or lending contracts + +The SDK will handle automatically when the `toToken` has a fungible ERC20 token, but sometimes there is no representation. In that case we can list the available integrations and specify that in the quote. + +```ts +const strategies = await gatewaySDK.getStrategies(); +const strategy = strategies.find(contract => contract.integration.name === "pell-wbtc")!; +const quoteParamsStaking: GatewayQuoteParams = { + ...quoteParams, + toChain: strategy.chain.chainId, + toToken: strategy.inputToken.symbol, // "wbtc" + strategyAddress: strategy.address, +}; +``` + ### Start the Order This locks in the quote, placing a hold on the LP's funds. Pass the same `quoteParams` as before and the `quote` returned from the previous step. @@ -97,7 +113,7 @@ We recommend using our [sats-wagmi](./sats-wagmi.md) package to interact with yo import { base64 } from "@scure/base"; import { Transaction } from "@scure/btc-signer"; -// NOTE: It is up to your implementation to sign the PSBT here! +// SIGN THIS! const tx = Transaction.fromPSBT(base64.decode(psbtBase64!)); ``` diff --git a/sdk/.prettierrc b/sdk/.prettierrc new file mode 100644 index 00000000..e8cc96a5 --- /dev/null +++ b/sdk/.prettierrc @@ -0,0 +1,5 @@ +{ + "tabWidth": 4, + "useTabs": false, + "printWidth": 120 +} \ No newline at end of file diff --git a/sdk/examples/gateway.ts b/sdk/examples/gateway.ts index 620edb37..c8de761c 100644 --- a/sdk/examples/gateway.ts +++ b/sdk/examples/gateway.ts @@ -9,6 +9,7 @@ export async function swapBtcForToken(evmAddress: string) { const quoteParams: GatewayQuoteParams = { fromChain: "Bitcoin", + fromToken: "BTC", fromUserAddress: "bc1qafk4yhqvj4wep57m62dgrmutldusqde8adh20d", toChain: "BOB", toUserAddress: evmAddress, diff --git a/sdk/package-lock.json b/sdk/package-lock.json index 6fcea513..0bdef204 100644 --- a/sdk/package-lock.json +++ b/sdk/package-lock.json @@ -1,12 +1,12 @@ { "name": "@gobob/bob-sdk", - "version": "2.0.0", + "version": "2.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@gobob/bob-sdk", - "version": "2.0.0", + "version": "2.2.0", "dependencies": { "@scure/base": "^1.1.7", "@scure/btc-signer": "^1.3.2", @@ -20,6 +20,7 @@ "ecpair": "^2.1.0", "mocha": "^10.7.3", "nock": "^14.0.0-beta.11", + "prettier": "3.3.3", "tiny-secp256k1": "^2.2.3", "ts-node": "^10.0.0", "typescript": "^5.5.4", @@ -60,7 +61,6 @@ "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", "dev": true, - "license": "MIT", "dependencies": { "@jridgewell/trace-mapping": "0.3.9" }, @@ -465,7 +465,6 @@ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "dev": true, - "license": "MIT", "engines": { "node": ">=6.0.0" } @@ -490,7 +489,6 @@ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", "dev": true, - "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" @@ -528,7 +526,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", - "license": "MIT", "engines": { "node": ">= 16" }, @@ -792,29 +789,25 @@ "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/@tsconfig/node12": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/@tsconfig/node14": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/@tsconfig/node16": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/@types/estree": { "version": "1.0.5", @@ -844,8 +837,7 @@ "version": "21.0.3", "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/@vitest/expect": { "version": "2.0.5", @@ -933,7 +925,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", "dev": true, - "license": "MIT", "bin": { "acorn": "bin/acorn" }, @@ -946,7 +937,6 @@ "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", "dev": true, - "license": "MIT", "engines": { "node": ">=0.4.0" } @@ -970,7 +960,6 @@ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, - "license": "MIT", "engines": { "node": ">=8" } @@ -980,7 +969,6 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, - "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, @@ -996,7 +984,6 @@ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", "dev": true, - "license": "ISC", "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" @@ -1009,15 +996,13 @@ "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" + "dev": true }, "node_modules/assertion-error": { "version": "2.0.1", @@ -1032,14 +1017,12 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/base-x": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/base-x/-/base-x-4.0.0.tgz", - "integrity": "sha512-FuwxlW4H5kh37X/oW59pwTzzTKRzfrrQwhmyspRM7swOEZcHtDZSCt45U6oKgtuFE+WYPblePMVIPR4RZrh/hw==", - "license": "MIT" + "integrity": "sha512-FuwxlW4H5kh37X/oW59pwTzzTKRzfrrQwhmyspRM7swOEZcHtDZSCt45U6oKgtuFE+WYPblePMVIPR4RZrh/hw==" }, "node_modules/base58-js": { "version": "1.0.5", @@ -1052,15 +1035,13 @@ "node_modules/bech32": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/bech32/-/bech32-2.0.0.tgz", - "integrity": "sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg==", - "license": "MIT" + "integrity": "sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg==" }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", "dev": true, - "license": "MIT", "engines": { "node": ">=8" }, @@ -1072,7 +1053,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/bip174/-/bip174-2.1.1.tgz", "integrity": "sha512-mdFV5+/v0XyNYXjBS6CQPLo9ekCx4gtKZFnJm5PMto7Fs9hTTDpkkzOB7/FtluRI6JbUUAu+snTYfJRgHLZbZQ==", - "license": "MIT", "engines": { "node": ">=8.0.0" } @@ -1108,7 +1088,6 @@ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, - "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -1129,14 +1108,12 @@ "version": "1.3.1", "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", - "dev": true, - "license": "ISC" + "dev": true }, "node_modules/bs58": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/bs58/-/bs58-5.0.0.tgz", "integrity": "sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ==", - "license": "MIT", "dependencies": { "base-x": "^4.0.0" } @@ -1145,7 +1122,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/bs58check/-/bs58check-3.0.1.tgz", "integrity": "sha512-hjuuJvoWEybo7Hn/0xOrczQKKEKD63WguEjlhLExYs2wUBcebDC1jDNK17eEAD2lYfw82d5ASC1d7K3SWszjaQ==", - "license": "MIT", "dependencies": { "@noble/hashes": "^1.2.0", "bs58": "^5.0.0" @@ -1165,7 +1141,6 @@ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", "dev": true, - "license": "MIT", "engines": { "node": ">=10" }, @@ -1194,7 +1169,6 @@ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, - "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -1211,7 +1185,6 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, - "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -1239,7 +1212,6 @@ "url": "https://paulmillr.com/funding/" } ], - "license": "MIT", "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -1261,7 +1233,6 @@ "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", "dev": true, - "license": "MIT", "dependencies": { "inherits": "^2.0.1", "safe-buffer": "^5.0.1" @@ -1272,7 +1243,6 @@ "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", "dev": true, - "license": "ISC", "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", @@ -1287,7 +1257,6 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, - "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -1299,15 +1268,13 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/create-hash": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", "dev": true, - "license": "MIT", "dependencies": { "cipher-base": "^1.0.1", "inherits": "^2.0.1", @@ -1320,15 +1287,13 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "dev": true, - "license": "MIT" + "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==", "dev": true, - "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -1359,15 +1324,13 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/decamelize": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", "dev": true, - "license": "MIT", "engines": { "node": ">=10" }, @@ -1398,7 +1361,6 @@ "resolved": "https://registry.npmjs.org/ecpair/-/ecpair-2.1.0.tgz", "integrity": "sha512-cL/mh3MtJutFOvFc27GPZE2pWL3a3k4YvzUWEOvilnfZVlH3Jwgx/7d6tlD7/75tNk8TG2m+7Kgtz0SI1tWcqw==", "dev": true, - "license": "MIT", "dependencies": { "randombytes": "^2.1.0", "typeforce": "^1.18.0", @@ -1412,8 +1374,7 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/esbuild": { "version": "0.21.5", @@ -1458,7 +1419,6 @@ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", "dev": true, - "license": "MIT", "engines": { "node": ">=6" } @@ -1468,7 +1428,6 @@ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, - "license": "MIT", "engines": { "node": ">=10" }, @@ -1544,7 +1503,6 @@ "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", "dev": true, - "license": "MIT", "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^8.0.1", @@ -1580,7 +1538,6 @@ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, - "license": "MIT", "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" @@ -1597,7 +1554,6 @@ "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", "dev": true, - "license": "BSD-3-Clause", "bin": { "flat": "cli.js" } @@ -1606,8 +1562,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, - "license": "ISC" + "dev": true }, "node_modules/fsevents": { "version": "2.3.3", @@ -1628,7 +1583,6 @@ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true, - "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" } @@ -1647,7 +1601,6 @@ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", "dev": true, - "license": "MIT", "engines": { "node": ">=16" }, @@ -1659,8 +1612,8 @@ "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, - "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -1680,7 +1633,6 @@ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, - "license": "ISC", "dependencies": { "is-glob": "^4.0.1" }, @@ -1693,7 +1645,6 @@ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, - "license": "MIT", "engines": { "node": ">=8" } @@ -1703,7 +1654,6 @@ "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz", "integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==", "dev": true, - "license": "MIT", "dependencies": { "inherits": "^2.0.4", "readable-stream": "^3.6.0", @@ -1718,7 +1668,6 @@ "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", "dev": true, - "license": "MIT", "bin": { "he": "bin/he" } @@ -1728,7 +1677,6 @@ "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", "dev": true, - "license": "Apache-2.0", "engines": { "node": ">=16.17.0" } @@ -1737,8 +1685,8 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", "dev": true, - "license": "ISC", "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -1748,15 +1696,13 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, - "license": "ISC" + "dev": true }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", "dev": true, - "license": "MIT", "dependencies": { "binary-extensions": "^2.0.0" }, @@ -1769,7 +1715,6 @@ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true, - "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -1779,7 +1724,6 @@ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true, - "license": "MIT", "engines": { "node": ">=8" } @@ -1789,7 +1733,6 @@ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, - "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" }, @@ -1817,7 +1760,6 @@ "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", "dev": true, - "license": "MIT", "engines": { "node": ">=8" } @@ -1827,7 +1769,6 @@ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", "dev": true, - "license": "MIT", "engines": { "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, @@ -1840,7 +1781,6 @@ "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", "dev": true, - "license": "MIT", "engines": { "node": ">=10" }, @@ -1852,15 +1792,13 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" + "dev": true }, "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dev": true, - "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, @@ -1879,7 +1817,6 @@ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, - "license": "MIT", "dependencies": { "p-locate": "^5.0.0" }, @@ -1895,7 +1832,6 @@ "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", "dev": true, - "license": "MIT", "dependencies": { "chalk": "^4.1.0", "is-unicode-supported": "^0.1.0" @@ -1929,15 +1865,13 @@ "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "dev": true, - "license": "ISC" + "dev": true }, "node_modules/md5.js": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", "dev": true, - "license": "MIT", "dependencies": { "hash-base": "^3.0.0", "inherits": "^2.0.1", @@ -1948,8 +1882,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/micro-packed": { "version": "0.6.3", @@ -1967,7 +1900,6 @@ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", "dev": true, - "license": "MIT", "engines": { "node": ">=12" }, @@ -2027,7 +1959,6 @@ "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", "dev": true, - "license": "ISC", "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", @@ -2039,7 +1970,6 @@ "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", "dev": true, - "license": "MIT", "dependencies": { "cliui": "^7.0.2", "escalade": "^3.1.1", @@ -2057,8 +1987,7 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/nanoid": { "version": "3.3.7", @@ -2097,7 +2026,6 @@ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "dev": true, - "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -2107,7 +2035,6 @@ "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", "dev": true, - "license": "MIT", "dependencies": { "path-key": "^4.0.0" }, @@ -2123,7 +2050,6 @@ "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", "dev": true, - "license": "MIT", "engines": { "node": ">=12" }, @@ -2136,7 +2062,6 @@ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "dev": true, - "license": "ISC", "dependencies": { "wrappy": "1" } @@ -2146,7 +2071,6 @@ "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", "dev": true, - "license": "MIT", "dependencies": { "mimic-fn": "^4.0.0" }, @@ -2163,28 +2087,11 @@ "integrity": "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==", "dev": true }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate/node_modules/p-limit": { + "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, - "license": "MIT", "dependencies": { "yocto-queue": "^0.1.0" }, @@ -2195,12 +2102,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/p-locate/node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, - "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, "engines": { "node": ">=10" }, @@ -2213,7 +2122,6 @@ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true, - "license": "MIT", "engines": { "node": ">=8" } @@ -2223,7 +2131,6 @@ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, - "license": "MIT", "engines": { "node": ">=8" } @@ -2254,7 +2161,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true, - "license": "MIT", "engines": { "node": ">=8.6" }, @@ -2290,6 +2196,21 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/prettier": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/propagate": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz", @@ -2304,7 +2225,6 @@ "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", "dev": true, - "license": "MIT", "dependencies": { "safe-buffer": "^5.1.0" } @@ -2314,7 +2234,6 @@ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "dev": true, - "license": "MIT", "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -2329,7 +2248,6 @@ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", "dev": true, - "license": "MIT", "dependencies": { "picomatch": "^2.2.1" }, @@ -2342,7 +2260,6 @@ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", "dev": true, - "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -2352,7 +2269,6 @@ "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", "dev": true, - "license": "MIT", "dependencies": { "hash-base": "^3.0.0", "inherits": "^2.0.1" @@ -2410,8 +2326,7 @@ "type": "consulting", "url": "https://feross.org/support" } - ], - "license": "MIT" + ] }, "node_modules/serialize-javascript": { "version": "6.0.2", @@ -2427,7 +2342,6 @@ "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", "dev": true, - "license": "(MIT AND BSD-3-Clause)", "dependencies": { "inherits": "^2.0.1", "safe-buffer": "^5.0.1" @@ -2446,7 +2360,6 @@ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, - "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" }, @@ -2459,7 +2372,6 @@ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, - "license": "MIT", "engines": { "node": ">=8" } @@ -2475,7 +2387,6 @@ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "dev": true, - "license": "ISC", "engines": { "node": ">=14" }, @@ -2502,8 +2413,7 @@ "version": "3.7.0", "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.7.0.tgz", "integrity": "sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/strict-event-emitter": { "version": "0.5.1", @@ -2516,7 +2426,6 @@ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", "dev": true, - "license": "MIT", "dependencies": { "safe-buffer": "~5.2.0" } @@ -2526,7 +2435,6 @@ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, - "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -2541,7 +2449,6 @@ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, - "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" }, @@ -2554,7 +2461,6 @@ "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", "dev": true, - "license": "MIT", "engines": { "node": ">=12" }, @@ -2567,7 +2473,6 @@ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true, - "license": "MIT", "engines": { "node": ">=8" }, @@ -2580,7 +2485,6 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, - "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -2596,7 +2500,6 @@ "resolved": "https://registry.npmjs.org/tiny-secp256k1/-/tiny-secp256k1-2.2.3.tgz", "integrity": "sha512-SGcL07SxcPN2nGKHTCvRMkQLYPSoeFcvArUSCYtjVARiFAWU44cCIqYS0mYAU6nY7XfvwURuTIGo2Omt3ZQr0Q==", "dev": true, - "license": "MIT", "dependencies": { "uint8array-tools": "0.0.7" }, @@ -2608,8 +2511,7 @@ "version": "2.8.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.8.0.tgz", "integrity": "sha512-1/eK7zUnIklz4JUUlL+658n58XO2hHLQfSk1Zf2LKieUjxidN16eKFEoDEfjHc3ohofSSqK3X5yO6VGb6iW8Lw==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/tinypool": { "version": "1.0.0", @@ -2655,7 +2557,6 @@ "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, - "license": "MIT", "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -2699,7 +2600,6 @@ "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", "dev": true, - "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" } @@ -2712,8 +2612,7 @@ "node_modules/typeforce": { "version": "1.18.0", "resolved": "https://registry.npmjs.org/typeforce/-/typeforce-1.18.0.tgz", - "integrity": "sha512-7uc1O8h1M1g0rArakJdf0uLRSSgFcYexrVoKo+bzJd32gd4gDy2L/Z+8/FjPnU9ydY3pEnVPtr9FyscYY60K1g==", - "license": "MIT" + "integrity": "sha512-7uc1O8h1M1g0rArakJdf0uLRSSgFcYexrVoKo+bzJd32gd4gDy2L/Z+8/FjPnU9ydY3pEnVPtr9FyscYY60K1g==" }, "node_modules/typescript": { "version": "5.5.4", @@ -2733,7 +2632,6 @@ "resolved": "https://registry.npmjs.org/uint8array-tools/-/uint8array-tools-0.0.7.tgz", "integrity": "sha512-vrrNZJiusLWoFWBqz5Y5KMCgP9W9hnjZHzZiZRT8oNAkq3d5Z5Oe76jAvVVSRh4U8GGR90N2X1dWtrhvx6L8UQ==", "dev": true, - "license": "MIT", "engines": { "node": ">=14.0.0" } @@ -2748,21 +2646,18 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/varuint-bitcoin": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/varuint-bitcoin/-/varuint-bitcoin-1.1.2.tgz", "integrity": "sha512-4EVb+w4rx+YfVM32HQX42AbbT7/1f5zwAYhIujKXKk8NQK+JfRVl3pqT3hjNn/L+RstigmGGKVwHA/P0wgITZw==", - "license": "MIT", "dependencies": { "safe-buffer": "^5.1.1" } @@ -2913,7 +2808,6 @@ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, - "license": "ISC", "dependencies": { "isexe": "^2.0.0" }, @@ -2945,7 +2839,6 @@ "resolved": "https://registry.npmjs.org/wif/-/wif-2.0.6.tgz", "integrity": "sha512-HIanZn1zmduSF+BQhkE+YXIbEiH0xPr1012QbFEGB0xsKqJii0/SqJjyn8dFv6y36kOznMgMB+LGcbZTJ1xACQ==", "dev": true, - "license": "MIT", "dependencies": { "bs58check": "<3.0.0" } @@ -2955,7 +2848,6 @@ "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.9.tgz", "integrity": "sha512-H7JU6iBHTal1gp56aKoaa//YUxEaAOUiydvrV/pILqIHXTtqxSkATOnDA2u+jZ/61sD+L/412+7kzXRtWukhpQ==", "dev": true, - "license": "MIT", "dependencies": { "safe-buffer": "^5.0.1" } @@ -2965,7 +2857,6 @@ "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz", "integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==", "dev": true, - "license": "MIT", "dependencies": { "base-x": "^3.0.2" } @@ -2975,7 +2866,6 @@ "resolved": "https://registry.npmjs.org/bs58check/-/bs58check-2.1.2.tgz", "integrity": "sha512-0TS1jicxdU09dwJMNZtVAfzPi6Q6QeN0pM1Fkzrjn+XYHvzMKPU3pHVpva+769iNVSfIYWf7LJ6WR+BuuMf8cA==", "dev": true, - "license": "MIT", "dependencies": { "bs58": "^4.0.0", "create-hash": "^1.1.0", @@ -2993,7 +2883,6 @@ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, - "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -3010,8 +2899,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, - "license": "ISC" + "dev": true }, "node_modules/ws": { "version": "8.17.1", @@ -3038,7 +2926,6 @@ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "dev": true, - "license": "ISC", "engines": { "node": ">=10" } @@ -3048,7 +2935,6 @@ "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "dev": true, - "license": "MIT", "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", @@ -3076,7 +2962,6 @@ "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", "dev": true, - "license": "MIT", "dependencies": { "camelcase": "^6.0.0", "decamelize": "^4.0.0", @@ -3092,7 +2977,6 @@ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", "dev": true, - "license": "ISC", "engines": { "node": ">=12" } @@ -3102,10 +2986,21 @@ "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", "dev": true, - "license": "MIT", "engines": { "node": ">=6" } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } } } diff --git a/sdk/package.json b/sdk/package.json index 53781694..cdef9f33 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -4,6 +4,8 @@ "main": "dist/index.js", "types": "dist/index.d.ts", "scripts": { + "format:check": "prettier --check src/**/*.ts", + "format:write": "prettier --write src/**/*.ts", "test": "vitest run test/*.ts", "deploy-relay": "ts-node src/scripts/relay-genesis.ts", "update-relay": "ts-node src/scripts/relay-retarget.ts", @@ -23,6 +25,7 @@ "ecpair": "^2.1.0", "mocha": "^10.7.3", "nock": "^14.0.0-beta.11", + "prettier": "3.3.3", "tiny-secp256k1": "^2.2.3", "ts-node": "^10.0.0", "typescript": "^5.5.4", @@ -36,4 +39,4 @@ "bitcoinjs-lib": "^6.1.6", "ethers": "^6.13.2" } -} \ No newline at end of file +} diff --git a/sdk/src/gateway/client.ts b/sdk/src/gateway/client.ts index 7a7820dd..b45e09c2 100644 --- a/sdk/src/gateway/client.ts +++ b/sdk/src/gateway/client.ts @@ -1,88 +1,22 @@ import { ethers, AbiCoder } from "ethers"; -import { GatewayQuoteParams } from "./types"; -import { SYMBOL_LOOKUP, ADDRESS_LOOKUP, Token as TokenInfo } from "./tokens"; +import { + GatewayQuoteParams, + GatewayQuote, + Chain, + ChainId, + Token, + GatewayStrategyContract, + GatewayCreateOrderRequest, + GatewayCreateOrderResponse, + GatewayOrder, + GatewayOrderResponse, + GatewayStartOrder, + GatewayStrategy, + EvmAddress, +} from "./types"; +import { SYMBOL_LOOKUP, ADDRESS_LOOKUP } from "./tokens"; import { createBitcoinPsbt } from "../wallet"; -export enum Chains { - // NOTE: we also support Bitcoin testnet - Bitcoin = "bitcoin", - BOB = "bob", - BOBSepolia = "bobsepolia", -}; - -type EvmAddress = string; - -type GatewayQuote = { - /** @description The gateway address */ - gatewayAddress: EvmAddress; - /** @description The minimum amount of Bitcoin to send */ - dustThreshold: number; - /** @description The satoshi output amount */ - satoshis: number; - /** @description The fee paid in satoshis (includes gas refill) */ - fee: number; - /** @description The Bitcoin address to send BTC */ - bitcoinAddress: string; - /** @description The number of confirmations required to confirm the Bitcoin tx */ - txProofDifficultyFactor: number; - /** @description The optional strategy address */ - strategyAddress?: EvmAddress, -}; - -/** @dev Internal request type used to call the Gateway API */ -type GatewayCreateOrderRequest = { - gatewayAddress: EvmAddress, - strategyAddress?: EvmAddress, - satsToConvertToEth: number, - userAddress: EvmAddress, - gatewayExtraData?: string, - strategyExtraData?: string, - satoshis: number, -}; - -type GatewayOrderResponse = { - /** @description The gateway address */ - gatewayAddress: EvmAddress; - /** @description The token address */ - tokenAddress: EvmAddress; - /** @description The Bitcoin txid */ - txid: string; - /** @description True when the order was executed on BOB */ - status: boolean; - /** @description When the order was created */ - timestamp: number; - /** @description The converted satoshi amount */ - tokens: string; - /** @description The satoshi output amount */ - satoshis: number; - /** @description The fee paid in satoshis (includes gas refill) */ - fee: number; - /** @description The number of confirmations required to confirm the Bitcoin tx */ - txProofDifficultyFactor: number; - /** @description The optional strategy address */ - strategyAddress?: EvmAddress, - /** @description The gas refill in satoshis */ - satsToConvertToEth: number, -}; - -/** Order given by the Gateway API once the bitcoin tx is submitted */ -type GatewayOrder = Omit; - -type GatewayCreateOrderResponse = { - uuid: string, - opReturnHash: string, -}; - -/** @dev The success type on create order */ -type GatewayStartOrderResult = GatewayCreateOrderResponse & { - bitcoinAddress: string, - satoshis: number; - psbtBase64?: string; -}; - type Optional = Omit & Partial; /** @@ -97,70 +31,102 @@ export const MAINNET_GATEWAY_BASE_URL = "https://gateway-api-mainnet.gobob.xyz"; */ export const TESTNET_GATEWAY_BASE_URL = "https://gateway-api-testnet.gobob.xyz"; -enum Network { - Mainnet, - Testnet, -} - /** - * Gateway REST HTTP API client + * Gateway REST HTTP API client */ export class GatewayApiClient { - private network: Network; + private chain: Chain.BOB | Chain.BOB_SEPOLIA; private baseUrl: string; /** * @constructor - * @param networkOrUrl The network ID or Gateway API URL. + * @param chainName The chain name. */ - constructor(networkOrUrl: string = "mainnet") { - switch (networkOrUrl) { + constructor(chainName: string) { + switch (chainName) { case "mainnet": - case "bob": - this.network = Network.Mainnet; + case Chain.BOB: + this.chain = Chain.BOB; this.baseUrl = MAINNET_GATEWAY_BASE_URL; break; case "testnet": - case "bobSepolia": - this.network = Network.Testnet; + case Chain.BOB_SEPOLIA: + this.chain = Chain.BOB_SEPOLIA; this.baseUrl = TESTNET_GATEWAY_BASE_URL; break; default: - this.baseUrl = networkOrUrl; + throw new Error("Invalid chain"); } } + /** + * Returns all chains supported by the SDK. + * + * @returns {string[]} The array of chain names. + */ + getChains(): string[] { + return Object.values(Chain); + } + /** * Get a quote from the Gateway API for swapping or staking BTC. - * + * * @param params The parameters for the quote. */ - async getQuote(params: Optional): Promise { - const isMainnet = params.toChain === 60808 || typeof params.toChain === "string" && params.toChain.toLowerCase() === Chains.BOB; - const isTestnet = params.toChain === 808813 || typeof params.toChain === "string" && params.toChain.toLowerCase() === Chains.BOBSepolia; + async getQuote( + params: Optional< + GatewayQuoteParams, + "amount" | "fromChain" | "fromToken" | "fromUserAddress" | "toUserAddress" + >, + ): Promise { + const isMainnet = + params.toChain === ChainId.BOB || + (typeof params.toChain === "string" && params.toChain.toLowerCase() === Chain.BOB); + const isTestnet = + params.toChain === ChainId.BOB_SEPOLIA || + (typeof params.toChain === "string" && params.toChain.toLowerCase() === Chain.BOB_SEPOLIA); + + const isInvalidNetwork = !isMainnet && !isTestnet; + const isMismatchMainnet = isMainnet && this.chain !== Chain.BOB; + const isMismatchTestnet = isTestnet && this.chain !== Chain.BOB_SEPOLIA; + + // TODO: switch URL if `toChain` is different chain? + if (isInvalidNetwork || isMismatchMainnet || isMismatchTestnet) { + throw new Error("Invalid output chain"); + } - const toToken = params.toToken.toLowerCase(); let outputToken = ""; + let strategyAddress: string | undefined; + + const toToken = params.toToken.toLowerCase(); + if (params.strategyAddress?.startsWith("0x")) { + strategyAddress = params.strategyAddress; + } + if (toToken.startsWith("0x")) { outputToken = toToken; - } else if (toToken in SYMBOL_LOOKUP) { - if (isMainnet && this.network === Network.Mainnet) { - outputToken = SYMBOL_LOOKUP[toToken].bob; - } else if (isTestnet && this.network === Network.Testnet) { - outputToken = SYMBOL_LOOKUP[toToken].bobSepolia; - } else { - throw new Error('Unknown network'); - } + } else if (isMainnet && this.chain === Chain.BOB && SYMBOL_LOOKUP[ChainId.BOB][toToken]) { + outputToken = SYMBOL_LOOKUP[ChainId.BOB][toToken].address; + } else if (isTestnet && this.chain === Chain.BOB_SEPOLIA && SYMBOL_LOOKUP[ChainId.BOB_SEPOLIA][toToken]) { + outputToken = SYMBOL_LOOKUP[ChainId.BOB_SEPOLIA][toToken].address; } else { - throw new Error('Unknown output token'); + throw new Error("Unknown output token"); } + var url = new URL(`${this.baseUrl}/quote/${outputToken}`); + if (strategyAddress) { + url.searchParams.append("strategy", `${strategyAddress}`); + } const atomicAmount = params.amount; - const response = await fetch(`${this.baseUrl}/quote/${outputToken}/${atomicAmount || ''}`, { + if (atomicAmount) { + url.searchParams.append("satoshis", `${atomicAmount}`); + } + + const response = await fetch(url, { headers: { - 'Content-Type': 'application/json', - Accept: 'application/json' - } + "Content-Type": "application/json", + Accept: "application/json", + }, }); return await response.json(); @@ -169,12 +135,15 @@ export class GatewayApiClient { // TODO: add error handling /** * Start an order via the Gateway API to reserve liquidity. This is step 1 of 2, see the {@link finalizeOrder} method. - * + * * @param gatewayQuote The quote given by the {@link getQuote} method. * @param params The parameters for the quote, same as before. - * @returns {Promise} The success object. + * @returns {Promise} The success object. */ - async startOrder(gatewayQuote: GatewayQuote, params: GatewayQuoteParams): Promise { + async startOrder( + gatewayQuote: GatewayQuote, + params: Optional, + ): Promise { const request: GatewayCreateOrderRequest = { gatewayAddress: gatewayQuote.gatewayAddress, strategyAddress: gatewayQuote.strategyAddress, @@ -187,33 +156,37 @@ export class GatewayApiClient { }; const response = await fetch(`${this.baseUrl}/order`, { - method: 'POST', + method: "POST", headers: { - 'Content-Type': 'application/json', - Accept: 'application/json' + "Content-Type": "application/json", + Accept: "application/json", }, - body: JSON.stringify(request) + body: JSON.stringify(request), }); if (!response.ok) { - throw new Error('Failed to create order'); + throw new Error("Failed to create order"); } const data: GatewayCreateOrderResponse = await response.json(); // NOTE: could remove this check but good for sanity if (data.opReturnHash != calculateOpReturnHash(request)) { - throw new Error('Invalid OP_RETURN hash'); + throw new Error("Invalid OP_RETURN hash"); } let psbtBase64: string; - if (params.fromUserAddress && typeof params.fromChain === "string" && params.fromChain.toLowerCase() === Chains.Bitcoin) { + if ( + params.fromUserAddress && + typeof params.fromChain === "string" && + params.fromChain.toLowerCase() === Chain.BITCOIN + ) { psbtBase64 = await createBitcoinPsbt( params.fromUserAddress, gatewayQuote.bitcoinAddress, gatewayQuote.satoshis, params.fromUserPublicKey, data.opReturnHash, - gatewayQuote.txProofDifficultyFactor + gatewayQuote.txProofDifficultyFactor, ); } @@ -223,78 +196,134 @@ export class GatewayApiClient { bitcoinAddress: gatewayQuote.bitcoinAddress, satoshis: gatewayQuote.satoshis, psbtBase64, - } + }; } /** * Finalize an order via the Gateway API by providing the Bitcoin transaction. The tx will * be validated for correctness and forwarded to the mempool so there is no need to separately * broadcast the transaction. This is step 2 of 2, see the {@link startOrder} method. - * + * * @param uuid The id given by the {@link startOrder} method. * @param bitcoinTxHex The hex encoded Bitcoin transaction. */ async finalizeOrder(uuid: string, bitcoinTxHex: string) { const response = await fetch(`${this.baseUrl}/order/${uuid}`, { - method: 'PATCH', + method: "PATCH", headers: { - 'Content-Type': 'application/json', - Accept: 'application/json' + "Content-Type": "application/json", + Accept: "application/json", }, - body: JSON.stringify({ bitcoinTx: bitcoinTxHex }) + body: JSON.stringify({ bitcoinTx: bitcoinTxHex }), }); if (!response.ok) { - throw new Error('Failed to update order'); + throw new Error("Failed to update order"); } } /** * Returns all pending and completed orders for this account. - * + * * @param userAddress The user's EVM address. * @returns {Promise} The array of account orders. */ async getOrders(userAddress: EvmAddress): Promise { - const response = await fetch(`${this.baseUrl}/orders/${userAddress}`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json' - } - }); - + const response = await this.fetchGet(`${this.baseUrl}/orders/${userAddress}`); const orders: GatewayOrderResponse[] = await response.json(); - return orders.map(order => { return { gasRefill: order.satsToConvertToEth, ...order } }); + return orders.map((order) => { + return { gasRefill: order.satsToConvertToEth, ...order }; + }); } /** - * Returns all tokens (and strategy tokens) supported by the Gateway API. - * - * @returns {Promise} The array of token addresses. + * Returns all strategies supported by the Gateway API. + * + * @returns {Promise} The array of strategies. */ - async getTokens(): Promise { - const response = await fetch(`${this.baseUrl}/tokens`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json' - } + async getStrategies(): Promise { + const response = await this.fetchGet(`${this.baseUrl}/strategies`); + + const chainName = (this.chain === Chain.BOB ? Chain.BOB : Chain.BOB_SEPOLIA).toString(); + const chainId = this.chain === Chain.BOB ? ChainId.BOB : ChainId.BOB_SEPOLIA; + + const strategies: GatewayStrategy[] = await response.json(); + return strategies.map((strategy) => { + const inputToken = ADDRESS_LOOKUP[strategy.inputTokenAddress]; + const outputToken = strategy.outputTokenAddress ? ADDRESS_LOOKUP[strategy.outputTokenAddress] : undefined; + return { + id: "", + type: "deposit", + address: strategy.strategyAddress, + method: "", + chain: { + id: "", // TODO + chainId: chainId, + slug: chainName, + name: chainName, + logo: "", // TODO + type: "evm", + singleChainSwap: true, + singleChainStaking: true, + }, + integration: { + type: strategy.strategyType, + slug: strategy.strategyName, + name: strategy.strategyName, + logo: "", // TODO + monetization: false, + }, + inputToken: { + symbol: inputToken.symbol, + address: inputToken.address, + logo: inputToken.logoURI, + decimals: inputToken.decimals, + chain: chainName, + }, + outputToken: outputToken + ? { + symbol: outputToken.symbol, + address: outputToken.address, + logo: outputToken.logoURI, + decimals: outputToken.decimals, + chain: chainName, + } + : null, + }; }); + } + /** + * Returns all tokens supported by the Gateway API. + * + * @param includeStrategies Also include output tokens via strategies (e.g. staking or lending). + * @returns {Promise} The array of token addresses. + */ + async getTokenAddresses(includeStrategies: boolean = true): Promise { + const response = await this.fetchGet(`${this.baseUrl}/tokens?includeStrategies=${includeStrategies}`); return response.json(); } /** - * Same as {@link getTokens} but with additional info. - * - * @returns {Promise} The array of tokens. + * Same as {@link getTokenAddresses} but with additional info. + * + * @param includeStrategies Also include output tokens via strategies (e.g. staking or lending). + * @returns {Promise} The array of tokens. */ - async getTokensInfo(): Promise { - const tokens = await this.getTokens(); - return tokens - .map(token => ADDRESS_LOOKUP[token]) - .filter(token => token !== undefined);; + async getTokens(includeStrategies: boolean = true): Promise { + // https://github.com/ethereum-optimism/ecosystem/blob/c6faa01455f9e846f31c0343a0be4c03cbeb2a6d/packages/op-app/src/hooks/useOPTokens.ts#L10 + const tokens = await this.getTokenAddresses(includeStrategies); + return tokens.map((token) => ADDRESS_LOOKUP[token]).filter((token) => token !== undefined); + } + + private async fetchGet(url: string) { + return await fetch(url, { + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + }); } } @@ -304,15 +333,17 @@ export class GatewayApiClient { */ function calculateOpReturnHash(req: GatewayCreateOrderRequest) { const abiCoder = new AbiCoder(); - return ethers.keccak256(abiCoder.encode( - ["address", "address", "uint256", "address", "bytes", "bytes"], - [ - req.gatewayAddress, - req.strategyAddress || ethers.ZeroAddress, - req.satsToConvertToEth, - req.userAddress, - req.gatewayExtraData || "0x", - req.strategyExtraData || "0x" - ] - )) -} \ No newline at end of file + return ethers.keccak256( + abiCoder.encode( + ["address", "address", "uint256", "address", "bytes", "bytes"], + [ + req.gatewayAddress, + req.strategyAddress || ethers.ZeroAddress, + req.satsToConvertToEth, + req.userAddress, + req.gatewayExtraData || "0x", + req.strategyExtraData || "0x", + ], + ), + ); +} diff --git a/sdk/src/gateway/index.ts b/sdk/src/gateway/index.ts index 692500ac..cf787bfb 100644 --- a/sdk/src/gateway/index.ts +++ b/sdk/src/gateway/index.ts @@ -1,2 +1,2 @@ export { GatewayApiClient as GatewaySDK } from "./client"; -export { GatewayQuoteParams } from "./types"; \ No newline at end of file +export { GatewayQuoteParams, GatewayQuote, GatewayOrder, GatewayStrategyContract } from "./types"; diff --git a/sdk/src/gateway/tokens.ts b/sdk/src/gateway/tokens.ts index bcbd1bec..979bff24 100644 --- a/sdk/src/gateway/tokens.ts +++ b/sdk/src/gateway/tokens.ts @@ -1,73 +1,103 @@ -export type Token = { - name: string, - symbol: string, - bob: string, - bobSepolia: string -} +import { ChainId, Token } from "./types"; -const TOKENS: { [key: string]: Token } = { - "tBTC": { +const TOKENS = [ + { name: "tBTC v2", symbol: "tBTC", + decimals: 18, bob: "0xBBa2eF945D523C4e2608C9E1214C2Cc64D4fc2e2", bobSepolia: "0x6744bAbDf02DCF578EA173A9F0637771A9e1c4d0", }, - "WBTC": { + { name: "Wrapped BTC", symbol: "WBTC", + decimals: 8, bob: "0x03C7054BCB39f7b2e5B2c7AcB37583e32D70Cfa3", bobSepolia: "0xe51e40e15e6e1496a0981f90Ca1D632545bdB519", }, - "sbtBTC": { + { name: "sb tBTC v2", symbol: "sbtBTC", + decimals: 8, bob: "0x2925dF9Eb2092B53B06A06353A7249aF3a8B139e", bobSepolia: "", }, - "sbWBTC": { + { name: "sb Wrapped BTC", symbol: "sbWBTC", + decimals: 8, bob: "0x5c46D274ed8AbCAe2964B63c0360ad3Ccc384dAa", bobSepolia: "", }, - "seTBTC": { + { name: "Segment TBTC", symbol: "seTBTC", + decimals: 8, bob: "0xD30288EA9873f376016A0250433b7eA375676077", bobSepolia: "", }, - "seWBTC": { + { name: "Segment WBTC", symbol: "seWBTC", + decimals: 8, bob: "0x6265C05158f672016B771D6Fb7422823ed2CbcDd", bobSepolia: "", }, - "stmtBTC": { + { name: "Staked mtBTC", symbol: "stmtBTC", + decimals: 18, bob: "", bobSepolia: "0xc4229678b65e2D9384FDf96F2E5D512d6eeC0C77", - } -}; + }, + { + name: "Solv BTC", + symbol: "SolvBTC", + decimals: 18, + bob: "0x541FD749419CA806a8bc7da8ac23D346f2dF8B77", + bobSepolia: "", + }, + { + name: "uniBTC", + symbol: "uniBTC", + decimals: 8, + bob: "0x236f8c0a61dA474dB21B693fB2ea7AAB0c803894", + bobSepolia: "", + }, +]; /** @description Tokens supported on BOB and BOB Sepolia */ -export const SYMBOL_LOOKUP: { [key: string]: Token } = {}; +export const SYMBOL_LOOKUP: { [key in number]: { [key in string]: Token } } = {}; export const ADDRESS_LOOKUP: { [address: string]: Token } = {}; -for (const key in TOKENS) { - const token = TOKENS[key]; +SYMBOL_LOOKUP[ChainId.BOB] = {}; +SYMBOL_LOOKUP[ChainId.BOB_SEPOLIA] = {}; - const lowerBob = token.bob.toLowerCase(); - const lowerBobSepolia = token.bobSepolia.toLowerCase(); +// TODO: re-write to use superchain tokenlist once supported +for (const token of TOKENS) { + const lowerAddressBob = token.bob.toLowerCase(); + const lowerTokenBob: Token = { + chainId: ChainId.BOB, + address: lowerAddressBob, + name: token.name, + symbol: token.symbol, + decimals: token.decimals, + logoURI: "", + }; - const lowercasedToken: Token = { + const lowerAddressBobSepolia = token.bobSepolia.toLowerCase(); + const lowerTokenBobSepolia: Token = { + chainId: ChainId.BOB_SEPOLIA, + address: lowerAddressBobSepolia, name: token.name, symbol: token.symbol, - bob: lowerBob, - bobSepolia: lowerBobSepolia, + decimals: token.decimals, + logoURI: "", }; - SYMBOL_LOOKUP[key.toLowerCase()] = lowercasedToken; - ADDRESS_LOOKUP[lowerBob] = lowercasedToken; - ADDRESS_LOOKUP[lowerBobSepolia] = lowercasedToken; -} \ No newline at end of file + SYMBOL_LOOKUP[ChainId.BOB][lowerTokenBob.symbol.toLowerCase()] = lowerTokenBob; + SYMBOL_LOOKUP[ChainId.BOB_SEPOLIA][lowerTokenBobSepolia.symbol.toLowerCase()] = lowerTokenBobSepolia; + + ADDRESS_LOOKUP[lowerAddressBob] = lowerTokenBob; + ADDRESS_LOOKUP[lowerAddressBobSepolia] = lowerTokenBobSepolia; +} diff --git a/sdk/src/gateway/types.ts b/sdk/src/gateway/types.ts index 4705791c..c2e5ec7f 100644 --- a/sdk/src/gateway/types.ts +++ b/sdk/src/gateway/types.ts @@ -1,23 +1,52 @@ type ChainSlug = string | number; type TokenSymbol = string; +export type EvmAddress = string; + +export enum Chain { + // NOTE: we also support Bitcoin testnet + BITCOIN = "bitcoin", + BOB = "bob", + BOB_SEPOLIA = "bob-sepolia", +} + +export enum ChainId { + BOB = 60808, + BOB_SEPOLIA = 808813, +} + +/** + * Designed to be compatible with the Superchain token list. + * https://github.com/ethereum-optimism/ethereum-optimism.github.io + */ +export interface Token { + chainId: number; + address: string; + name: string; + symbol: string; + decimals: number; + logoURI: string; +} + +/** + * Designed to be compatible with the Swing SDK. + * https://developers.swing.xyz/reference/sdk/get-a-quote + */ export interface GatewayQuoteParams { /** @description Source chain slug or ID */ - fromChain?: ChainSlug; + fromChain: ChainSlug; /** @description Destination chain slug or ID */ toChain: ChainSlug; /** @description Token symbol or address on source chain */ - fromToken?: TokenSymbol; + fromToken: TokenSymbol; /** @description Token symbol or address on destination chain */ toToken: TokenSymbol; /** @description Wallet address on source chain */ - fromUserAddress?: string; - /** @description Wallet public key on source chain */ - fromUserPublicKey?: string; + fromUserAddress: string; /** @description Wallet address on destination chain */ toUserAddress: string; /** @description Amount of tokens to send from the source chain */ - amount: number | string; + amount: number | string; // NOTE: modified from Swing /** @description Maximum slippage percentage between 0.01 and 0.03 (Default: 0.03) */ maxSlippage?: number; @@ -25,10 +54,189 @@ export interface GatewayQuoteParams { /** @description Unique affiliate ID for tracking */ affiliateId?: string; /** @description Optionally filter the type of routes returned */ - type?: 'swap' | 'deposit' | 'withdraw' | 'claim'; + type?: "swap" | "deposit" | "withdraw" | "claim"; /** @description The percentage of fee charged by partners in Basis Points (BPS) units. This will override the default fee rate configured via platform. 1 BPS = 0.01%. The maximum value is 1000 (which equals 10%). The minimum value is 1 (which equals 0.01%). */ fee?: number; + // NOTE: the following are new fields added by us /** @description Amount of satoshis to swap for ETH */ - gasRefill?: number, + gasRefill?: number; + /** @description Wallet public key on source chain */ + fromUserPublicKey?: string; + /** @description Strategy address */ + strategyAddress?: string; +} + +/** + * IntegrationType + * @enum {string} + */ +type GatewayIntegrationType = "bridge" | "dex" | "staking" | "lending"; + +interface GatewayIntegration { + type: GatewayIntegrationType; + /** @example rocketpool */ + slug: string; + /** @example RocketPool */ + name: string; + /** Format: uri */ + logo: string; + monetization: boolean; +} + +type GatewayStrategyType = "deposit" | "withdraw" | "claim" | "router" | "bridge"; + +interface GatewayToken { + /** @example ETH */ + symbol: string; + /** @example 0x000000000000000 */ + address: string; + /** @example https://raw.githubusercontent.com/bob-collective/assets/master/blockchains/ethereum/assets/0x0000000000000000000000000000000000000000/logo.png */ + logo: string; + /** @example 18 */ + decimals: number; + /** @example ethereum */ + chain: string; +} + +type GatewayChainType = "evm" | "ibc" | "solana" | "multiversx" | "bitcoin" | "ton" | "tron"; + +interface GatewayChain { + id: string; + chainId: number; + /** @example ethereum */ + slug: string; + /** @example Ethereum */ + name: string; + /** @example https://raw.githubusercontent.com/bob-collective/assets/master/blockchains/ethereum/info/logo.png */ + logo: string; + type: GatewayChainType; + /** @description Single chain swapping is supported for this chain. */ + singleChainSwap: boolean; + /** @description Single chain staking is supported for this chain. */ + singleChainStaking: boolean; + nativeToken?: GatewayToken; + /** + * @description URL template to transaction details. + * @example https://etherscan.io/tx/{txHash} + */ + txExplorer?: string; + /** + * @description URL template to token details. + * @example https://etherscan.io/tokens/{address} + */ + tokenExplorer?: string; + /** + * @description URL template to RPC endpoint. + * @example https://eth-mainnet.g.alchemy.com/v2/xxx + */ + rpcUrl?: string; +} + +/** + * Designed to be compatible with the Swing SDK. + * https://developers.swing.xyz/reference/sdk/staking/contracts + */ +export interface GatewayStrategyContract { + id: string; + type: GatewayStrategyType; + /** + * @description Contract address + * @example 0x... + */ + address: string; + /** @example deposit */ + method: string; + /** @example bob */ + chain: GatewayChain; + /** @example segment */ + integration: GatewayIntegration; + + inputToken: GatewayToken; + /** @example seWBTC */ + outputToken: GatewayToken | null; +} + +export type GatewayQuote = { + /** @description The gateway address */ + gatewayAddress: EvmAddress; + /** @description The minimum amount of Bitcoin to send */ + dustThreshold: number; + /** @description The satoshi output amount */ + satoshis: number; + /** @description The fee paid in satoshis (includes gas refill) */ + fee: number; + /** @description The Bitcoin address to send BTC */ + bitcoinAddress: string; + /** @description The number of confirmations required to confirm the Bitcoin tx */ + txProofDifficultyFactor: number; + /** @description The optional strategy address */ + strategyAddress?: EvmAddress; +}; + +/** @dev Internal */ +export type GatewayCreateOrderRequest = { + gatewayAddress: EvmAddress; + strategyAddress?: EvmAddress; + satsToConvertToEth: number; + userAddress: EvmAddress; + gatewayExtraData?: string; + strategyExtraData?: string; + satoshis: number; +}; + +export type GatewayOrderResponse = { + /** @description The gateway address */ + gatewayAddress: EvmAddress; + /** @description The token address */ + tokenAddress: EvmAddress; + /** @description The Bitcoin txid */ + txid: string; + /** @description True when the order was executed on BOB */ + status: boolean; + /** @description When the order was created */ + timestamp: number; + /** @description The converted satoshi amount */ + tokens: string; + /** @description The satoshi output amount */ + satoshis: number; + /** @description The fee paid in satoshis (includes gas refill) */ + fee: number; + /** @description The number of confirmations required to confirm the Bitcoin tx */ + txProofDifficultyFactor: number; + /** @description The optional strategy address */ + strategyAddress?: EvmAddress; + /** @description The gas refill in satoshis */ + satsToConvertToEth: number; +}; + +/** Order given by the Gateway API once the bitcoin tx is submitted */ +export type GatewayOrder = Omit< + GatewayOrderResponse & { + /** @description The gas refill in satoshis */ + gasRefill: number; + }, + "satsToConvertToEth" +>; + +/** @dev Internal */ +export type GatewayCreateOrderResponse = { + uuid: string; + opReturnHash: string; +}; + +/** @dev The success type on create order */ +export type GatewayStartOrder = GatewayCreateOrderResponse & { + bitcoinAddress: string; + satoshis: number; + psbtBase64?: string; +}; + +/** @dev Internal */ +export interface GatewayStrategy { + strategyAddress: string; + strategyName: string; + strategyType: "staking" | "lending"; + inputTokenAddress: string; + outputTokenAddress?: string; } diff --git a/sdk/src/ordinal-api/index.ts b/sdk/src/ordinal-api/index.ts index 332d020c..3046cc4e 100644 --- a/sdk/src/ordinal-api/index.ts +++ b/sdk/src/ordinal-api/index.ts @@ -41,7 +41,7 @@ export module InscriptionId { export type OutPoint = { txid: string; vout: number; -} +}; export module OutPoint { export function toString(id: OutPoint): string { @@ -62,7 +62,7 @@ export module OutPoint { export type SatPoint = { outpoint: OutPoint; offset: number; -} +}; export module SatPoint { export function toString(id: SatPoint): string { @@ -128,10 +128,10 @@ export interface OutputJson { */ runes: { [key: string]: { - amount: number, - divisibility: number, - symbol: string | null, - } + amount: number; + divisibility: number; + symbol: string | null; + }; }; /** @@ -190,7 +190,7 @@ export interface SatJson { */ block: number; - charms: number[], + charms: number[]; /** * The cycle associated with the ordinal. @@ -248,7 +248,7 @@ export interface InscriptionJson { */ address: string | null; - charms: string[], + charms: string[]; /** * An array of child IDs. @@ -265,7 +265,7 @@ export interface InscriptionJson { */ content_type: string | null; - effective_content_type: String | null, + effective_content_type: String | null; /** * The genesis fee of the inscription. @@ -366,15 +366,17 @@ export class OrdinalsClient { * ``` */ async getInscriptionFromId(id: InscriptionId): Promise> { - console.log(`${this.basePath}/inscription/${InscriptionId.toString(id)}`) - const inscriptionJson = await this.getJson>(`${this.basePath}/inscription/${InscriptionId.toString(id)}`); + console.log(`${this.basePath}/inscription/${InscriptionId.toString(id)}`); + const inscriptionJson = await this.getJson>( + `${this.basePath}/inscription/${InscriptionId.toString(id)}`, + ); return { ...inscriptionJson, children: inscriptionJson.children.map(InscriptionId.fromString), id: InscriptionId.fromString(inscriptionJson.id), - next: (inscriptionJson.next != null) ? InscriptionId.fromString(inscriptionJson.next) : null, - parent: (inscriptionJson.parent != null) ? InscriptionId.fromString(inscriptionJson.parent) : null, - previous: (inscriptionJson.previous != null) ? InscriptionId.fromString(inscriptionJson.previous) : null, + next: inscriptionJson.next != null ? InscriptionId.fromString(inscriptionJson.next) : null, + parent: inscriptionJson.parent != null ? InscriptionId.fromString(inscriptionJson.parent) : null, + previous: inscriptionJson.previous != null ? InscriptionId.fromString(inscriptionJson.previous) : null, satpoint: SatPoint.fromString(inscriptionJson.satpoint), }; } @@ -410,7 +412,9 @@ export class OrdinalsClient { * ``` */ async getInscriptionsFromBlock(height: number): Promise> { - const inscriptionsJson = await this.getJson>(`${this.basePath}/inscriptions/block/${height}`); + const inscriptionsJson = await this.getJson>( + `${this.basePath}/inscriptions/block/${height}`, + ); return this.parseInscriptionsJson(inscriptionsJson); } @@ -449,7 +453,7 @@ export class OrdinalsClient { const satJson = await this.getJson>(`${this.basePath}/sat/${sat}`); return { ...satJson, - inscriptions: satJson.inscriptions.map(id => InscriptionId.fromString(id)), + inscriptions: satJson.inscriptions.map((id) => InscriptionId.fromString(id)), }; } @@ -467,7 +471,9 @@ export class OrdinalsClient { * ``` */ async getInscriptionsFromStartBlock(startHeight: number): Promise> { - const inscriptionsJson = await this.getJson>(`${this.basePath}/inscriptions/${startHeight}`); + const inscriptionsJson = await this.getJson>( + `${this.basePath}/inscriptions/${startHeight}`, + ); return this.parseInscriptionsJson(inscriptionsJson); } @@ -477,20 +483,20 @@ export class OrdinalsClient { private async getJson(url: string): Promise { const response = await fetch(url, { headers: { - 'Accept': 'application/json', + Accept: "application/json", }, }); if (!response.ok) { throw new Error(response.statusText); } - return await response.json() as Promise; + return (await response.json()) as Promise; } /** * @ignore */ private parseInscriptionsJson(inscriptionsJson: InscriptionsJson): InscriptionsJson { - const ids = inscriptionsJson.ids.map(id => InscriptionId.fromString(id)); + const ids = inscriptionsJson.ids.map((id) => InscriptionId.fromString(id)); return { ...inscriptionsJson, ids, diff --git a/sdk/src/ordinals/commit.ts b/sdk/src/ordinals/commit.ts index 0423b86c..9d292cb9 100644 --- a/sdk/src/ordinals/commit.ts +++ b/sdk/src/ordinals/commit.ts @@ -7,7 +7,7 @@ export interface CommitTxData { leafVersion: number; script: Buffer; controlBlock: Buffer; - } + }; } function toXOnly(pubkey: Buffer) { @@ -21,7 +21,7 @@ function toXOnly(pubkey: Buffer) { export function createCommitTxData( network: bitcoin.Network, publicKey: Buffer, - inscription: Inscription + inscription: Inscription, ): CommitTxData { const xOnlyPublicKey = toXOnly(publicKey); const script = inscription.toScript(xOnlyPublicKey); @@ -53,4 +53,3 @@ export function createCommitTxData( tapLeafScript, }; } - diff --git a/sdk/src/ordinals/index.ts b/sdk/src/ordinals/index.ts index a2ce53cd..8a748dbb 100644 --- a/sdk/src/ordinals/index.ts +++ b/sdk/src/ordinals/index.ts @@ -11,35 +11,35 @@ export { RemoteSigner }; * Estimate the virtual size of a 1 input 1 output reveal tx. */ function estimateTxSize( - network: bitcoin.Network, - publicKey: Buffer, - commitTxData: CommitTxData, - toAddress: string, - amount: number, + network: bitcoin.Network, + publicKey: Buffer, + commitTxData: CommitTxData, + toAddress: string, + amount: number, ) { - const psbt = new bitcoin.Psbt({ network }); - - const { scriptTaproot, tapLeafScript } = commitTxData; - psbt.addInput({ - hash: Buffer.alloc(32, 0), - index: 0, - witnessUtxo: { - value: amount, - script: scriptTaproot.output!, - }, - tapLeafScript: [tapLeafScript], - }); - - psbt.addOutput({ - value: amount, - address: toAddress, - }); - - psbt.signInput(0, new DummySigner(publicKey)); - psbt.finalizeInput(0, customFinalizer(commitTxData)); - - const tx = psbt.extractTransaction(); - return tx.virtualSize(); + const psbt = new bitcoin.Psbt({ network }); + + const { scriptTaproot, tapLeafScript } = commitTxData; + psbt.addInput({ + hash: Buffer.alloc(32, 0), + index: 0, + witnessUtxo: { + value: amount, + script: scriptTaproot.output!, + }, + tapLeafScript: [tapLeafScript], + }); + + psbt.addOutput({ + value: amount, + address: toAddress, + }); + + psbt.signInput(0, new DummySigner(publicKey)); + psbt.finalizeInput(0, customFinalizer(commitTxData)); + + const tx = psbt.extractTransaction(); + return tx.virtualSize(); } /** @@ -53,50 +53,40 @@ function estimateTxSize( * @returns Promise which resolves to the reveal transaction. */ export async function inscribeData( - signer: RemoteSigner, - toAddress: string, - feeRate: number, - inscription: Inscription, - postage = 10000, + signer: RemoteSigner, + toAddress: string, + feeRate: number, + inscription: Inscription, + postage = 10000, ) { - const bitcoinNetwork = await signer.getNetwork(); - const publicKey = Buffer.from(await signer.getPublicKey(), "hex"); - - const commitTxData = createCommitTxData(bitcoinNetwork, publicKey, inscription); - - const revealTxSize = estimateTxSize(bitcoinNetwork, publicKey, commitTxData, toAddress, postage); - - // https://github.com/ordinals/ord/blob/ea1c7c8f73e1c30df547000ac7ccd82051cb60af/src/subcommand/wallet/inscribe/batch.rs#L501 - const revealFee = revealTxSize * feeRate; - // https://github.com/ordinals/ord/blob/ea1c7c8f73e1c30df547000ac7ccd82051cb60af/src/subcommand/wallet/inscribe/batch.rs#L327 - const commitTxAmount = revealFee + postage; - - const commitAddress = commitTxData.scriptTaproot.address!; - const commitTxId = await signer.sendToAddress(commitAddress, commitTxAmount); - const commitTx = await signer.getTransaction(commitTxId); - - const scriptPubKey = bitcoin.address.toOutputScript(commitAddress, bitcoinNetwork); - const commitUtxoIndex = commitTx.outs.findIndex(out => out.script.equals(scriptPubKey)); - - const commitTxResult = { - tx: commitTx, - outputIndex: commitUtxoIndex, - outputAmount: commitTxAmount, - }; - - const revealPsbt = createRevealTx( - bitcoinNetwork, - commitTxData, - commitTxResult, - toAddress, - postage, - ); - - const revealTx = await signRevealTx( - signer, - commitTxData, - revealPsbt - ); - - return revealTx; + const bitcoinNetwork = await signer.getNetwork(); + const publicKey = Buffer.from(await signer.getPublicKey(), "hex"); + + const commitTxData = createCommitTxData(bitcoinNetwork, publicKey, inscription); + + const revealTxSize = estimateTxSize(bitcoinNetwork, publicKey, commitTxData, toAddress, postage); + + // https://github.com/ordinals/ord/blob/ea1c7c8f73e1c30df547000ac7ccd82051cb60af/src/subcommand/wallet/inscribe/batch.rs#L501 + const revealFee = revealTxSize * feeRate; + // https://github.com/ordinals/ord/blob/ea1c7c8f73e1c30df547000ac7ccd82051cb60af/src/subcommand/wallet/inscribe/batch.rs#L327 + const commitTxAmount = revealFee + postage; + + const commitAddress = commitTxData.scriptTaproot.address!; + const commitTxId = await signer.sendToAddress(commitAddress, commitTxAmount); + const commitTx = await signer.getTransaction(commitTxId); + + const scriptPubKey = bitcoin.address.toOutputScript(commitAddress, bitcoinNetwork); + const commitUtxoIndex = commitTx.outs.findIndex((out) => out.script.equals(scriptPubKey)); + + const commitTxResult = { + tx: commitTx, + outputIndex: commitUtxoIndex, + outputAmount: commitTxAmount, + }; + + const revealPsbt = createRevealTx(bitcoinNetwork, commitTxData, commitTxResult, toAddress, postage); + + const revealTx = await signRevealTx(signer, commitTxData, revealPsbt); + + return revealTx; } diff --git a/sdk/src/ordinals/reveal.ts b/sdk/src/ordinals/reveal.ts index 4249b714..3831d2f0 100644 --- a/sdk/src/ordinals/reveal.ts +++ b/sdk/src/ordinals/reveal.ts @@ -57,13 +57,9 @@ export const customFinalizer = (commitTxData: CommitTxData) => { finalScriptWitness: witnessStackToScriptWitness(witness), }; }; -} +}; -export async function signRevealTx( - signer: RemoteSigner, - commitTxData: CommitTxData, - psbt: bitcoin.Psbt -) { +export async function signRevealTx(signer: RemoteSigner, commitTxData: CommitTxData, psbt: bitcoin.Psbt) { // reveal should only have one input psbt = await signer.signInput(0, psbt); @@ -71,4 +67,4 @@ export async function signRevealTx( psbt.finalizeInput(0, customFinalizer(commitTxData)); return psbt.extractTransaction(); -} \ No newline at end of file +} diff --git a/sdk/src/ordinals/signer.ts b/sdk/src/ordinals/signer.ts index f400e135..be5eab38 100644 --- a/sdk/src/ordinals/signer.ts +++ b/sdk/src/ordinals/signer.ts @@ -54,4 +54,4 @@ export interface RemoteSigner { * @returns {Promise} A promise that resolves to the signed PSBT. */ signInput(inputIndex: number, psbt: Psbt): Promise; -}; +} diff --git a/sdk/src/scripts/relay-genesis.ts b/sdk/src/scripts/relay-genesis.ts index 73e578c7..7eda7d8a 100644 --- a/sdk/src/scripts/relay-genesis.ts +++ b/sdk/src/scripts/relay-genesis.ts @@ -37,8 +37,7 @@ const args = yargs(hideBin(process.argv)) .option("verifier-url", { description: "Verifier URL", type: "string", - }) - .argv; + }).argv; main().catch((err) => { console.log("Error thrown by script:"); @@ -47,12 +46,16 @@ main().catch((err) => { }); function range(size: number, startAt = 0) { - return [...Array(size).keys()].map(i => i + startAt); + return [...Array(size).keys()].map((i) => i + startAt); } async function getRetargetHeaders(esploraClient: EsploraClient, nextRetargetHeight: number, proofLength: number) { - const beforeRetarget = await Promise.all(range(proofLength, nextRetargetHeight - proofLength).map(height => esploraClient.getBlockHeaderAt(height))); - const afterRetarget = await Promise.all(range(proofLength, nextRetargetHeight).map(height => esploraClient.getBlockHeaderAt(height))); + const beforeRetarget = await Promise.all( + range(proofLength, nextRetargetHeight - proofLength).map((height) => esploraClient.getBlockHeaderAt(height)), + ); + const afterRetarget = await Promise.all( + range(proofLength, nextRetargetHeight).map((height) => esploraClient.getBlockHeaderAt(height)), + ); return beforeRetarget.concat(afterRetarget).join(""); } @@ -63,9 +66,9 @@ async function main(): Promise { if (initHeight == "latest") { const currentHeight = await esploraClient.getLatestHeight(); initHeight = currentHeight - (currentHeight % 2016) - 2016; - console.log(`Using block ${initHeight}`) + console.log(`Using block ${initHeight}`); } - if ((initHeight % 2016) != 0) { + if (initHeight % 2016 != 0) { throw new Error("Invalid genesis height: must be multiple of 2016"); } @@ -113,7 +116,8 @@ async function main(): Promise { env.push("TESTNET=true"); } - exec(`${env.join(" ")} forge script ../script/RelayGenesis.s.sol:RelayGenesisScript --rpc-url '${rpcUrl}' ${verifyOpts} --broadcast --priority-gas-price 1`, + exec( + `${env.join(" ")} forge script ../script/RelayGenesis.s.sol:RelayGenesisScript --rpc-url '${rpcUrl}' ${verifyOpts} --broadcast --priority-gas-price 1`, { maxBuffer: 1024 * 5000 }, (err: any, stdout: string, stderr: string) => { if (err) { @@ -123,5 +127,6 @@ async function main(): Promise { // the *entire* stdout and stderr (buffered) console.log(`stdout: ${stdout}`); console.log(`stderr: ${stderr}`); - }); + }, + ); } diff --git a/sdk/src/scripts/relay-retarget.ts b/sdk/src/scripts/relay-retarget.ts index 6c7d825e..07325979 100644 --- a/sdk/src/scripts/relay-retarget.ts +++ b/sdk/src/scripts/relay-retarget.ts @@ -4,7 +4,7 @@ import { hideBin } from "yargs/helpers"; import { exec } from "node:child_process"; const args = yargs(hideBin(process.argv)) - .env('RELAY') + .env("RELAY") .option("private-key", { description: "Private key to submit with", type: "string", @@ -26,8 +26,7 @@ const args = yargs(hideBin(process.argv)) description: "Relay address", type: "string", demandOption: true, - }) - .argv; + }).argv; main().catch((err) => { console.log("Error thrown by script:"); @@ -36,12 +35,16 @@ main().catch((err) => { }); function range(size: number, startAt = 0) { - return [...Array(size).keys()].map(i => i + startAt); + return [...Array(size).keys()].map((i) => i + startAt); } async function getRetargetHeaders(esploraClient: EsploraClient, nextRetargetHeight: number, proofLength: number) { - const beforeRetarget = await Promise.all(range(proofLength, nextRetargetHeight - proofLength).map(height => esploraClient.getBlockHeaderAt(height))); - const afterRetarget = await Promise.all(range(proofLength, nextRetargetHeight).map(height => esploraClient.getBlockHeaderAt(height))); + const beforeRetarget = await Promise.all( + range(proofLength, nextRetargetHeight - proofLength).map((height) => esploraClient.getBlockHeaderAt(height)), + ); + const afterRetarget = await Promise.all( + range(proofLength, nextRetargetHeight).map((height) => esploraClient.getBlockHeaderAt(height)), + ); return beforeRetarget.concat(afterRetarget).join(""); } @@ -71,20 +74,24 @@ async function main(): Promise { } const currentEpoch = await new Promise((resolve, reject) => { - exec(`cast call ${relayAddress} "currentEpoch() (uint256)" --rpc-url '${rpcUrl}'`, + exec( + `cast call ${relayAddress} "currentEpoch() (uint256)" --rpc-url '${rpcUrl}'`, (err: any, stdout: string, _stderr: string) => { if (err) reject(`Failed to run command: ${err}`); resolve(Number.parseInt(stdout)); - }); + }, + ); }); console.log(`Current epoch: ${currentEpoch}`); const proofLength = await new Promise((resolve, reject) => { - exec(`cast call ${relayAddress} "proofLength() (uint256)" --rpc-url '${rpcUrl}'`, + exec( + `cast call ${relayAddress} "proofLength() (uint256)" --rpc-url '${rpcUrl}'`, (err: any, stdout: string, _stderr: string) => { if (err) reject(`Failed to run command: ${err}`); resolve(Number.parseInt(stdout)); - }); + }, + ); }); console.log(`Proof length: ${proofLength}`); @@ -104,12 +111,13 @@ async function main(): Promise { const retargetHeaders = await getRetargetHeaders(esploraClient, nextRetargetHeight, proofLength); let env = { - 'RELAY_ADDRESS': relayAddress, - 'RETARGET_HEADERS': retargetHeaders, - 'PRIVATE_KEY': privateKey, + RELAY_ADDRESS: relayAddress, + RETARGET_HEADERS: retargetHeaders, + PRIVATE_KEY: privateKey, }; - exec(`forge script ../script/RelayRetarget.s.sol:RelayRetargetScript --rpc-url '${rpcUrl}' --broadcast --priority-gas-price 1`, + exec( + `forge script ../script/RelayRetarget.s.sol:RelayRetargetScript --rpc-url '${rpcUrl}' --broadcast --priority-gas-price 1`, { env: { ...process.env, ...env } }, (err: any, stdout: string, stderr: string) => { if (err) { @@ -119,5 +127,6 @@ async function main(): Promise { // the *entire* stdout and stderr (buffered) console.log(`stdout: ${stdout}`); console.log(`stderr: ${stderr}`); - }); + }, + ); } diff --git a/sdk/src/wallet/index.ts b/sdk/src/wallet/index.ts index a7ff86bd..29f4ff1f 100644 --- a/sdk/src/wallet/index.ts +++ b/sdk/src/wallet/index.ts @@ -1,3 +1,3 @@ export * from "./utxo"; -export { validate as isValidBtcAddress } from 'bitcoin-address-validation'; +export { validate as isValidBtcAddress } from "bitcoin-address-validation"; diff --git a/sdk/src/wallet/inscriptions.ts b/sdk/src/wallet/inscriptions.ts index 80b4fe2d..54d076fa 100644 --- a/sdk/src/wallet/inscriptions.ts +++ b/sdk/src/wallet/inscriptions.ts @@ -6,22 +6,22 @@ export async function findUtxoForInscriptionId( esploraClient: EsploraClient, ordinalsClient: OrdinalsClient, utxos: UTXO[], - inscriptionId: string + inscriptionId: string, ): Promise { // TODO: can we get the current UTXO of the inscription from ord? // we can use the satpoint for this - const { txid, index } = InscriptionId.fromString(inscriptionId) + const { txid, index } = InscriptionId.fromString(inscriptionId); for (const utxo of utxos) { if (utxo.confirmed) { - const inscriptionUtxo = await ordinalsClient.getInscriptionsFromOutPoint(utxo) + const inscriptionUtxo = await ordinalsClient.getInscriptionsFromOutPoint(utxo); if (inscriptionUtxo.inscriptions && inscriptionUtxo.inscriptions.includes(inscriptionId)) { return utxo; } } else if (txid == utxo.txid) { const inscriptions = await getTxInscriptions(esploraClient, utxo.txid); - if (typeof inscriptions[index] !== 'undefined') { + if (typeof inscriptions[index] !== "undefined") { return utxo; } } @@ -41,14 +41,14 @@ export async function findUtxosWithoutInscriptions(network: string, utxos: UTXO[ if (utxo.confirmed) { const inscription = await ordinalsClient.getInscriptionsFromOutPoint({ txid: utxo.txid, - vout: utxo.vout + vout: utxo.vout, }); if (inscription.inscriptions.length === 0) { safeUtxos.push(utxo); } } - }) + }), ]); return safeUtxos; diff --git a/sdk/src/wallet/utxo.ts b/sdk/src/wallet/utxo.ts index e5ac8678..47ac5bb4 100644 --- a/sdk/src/wallet/utxo.ts +++ b/sdk/src/wallet/utxo.ts @@ -1,38 +1,38 @@ -import { Transaction, Script, selectUTXO, TEST_NETWORK, NETWORK, p2wpkh, p2sh } from '@scure/btc-signer'; +import { Transaction, Script, selectUTXO, TEST_NETWORK, NETWORK, p2wpkh, p2sh } from "@scure/btc-signer"; import { hex, base64 } from "@scure/base"; -import { AddressType, getAddressInfo, Network } from 'bitcoin-address-validation'; -import { EsploraClient, UTXO } from '../esplora'; +import { AddressType, getAddressInfo, Network } from "bitcoin-address-validation"; +import { EsploraClient, UTXO } from "../esplora"; export type BitcoinNetworkName = Exclude; const bitcoinNetworks: Record = { - mainnet: NETWORK, - testnet: TEST_NETWORK + mainnet: NETWORK, + testnet: TEST_NETWORK, }; export const getBtcNetwork = (name: BitcoinNetworkName) => { - return bitcoinNetworks[name]; + return bitcoinNetworks[name]; }; type Output = { address: string; amount: bigint } | { script: Uint8Array; amount: bigint }; export interface Input { - txid: string; - index: number; - witness_script?: Uint8Array; - redeemScript?: Uint8Array; - witnessUtxo?: { - script: Uint8Array; - amount: bigint; - }; - nonWitnessUtxo?: Uint8Array; + txid: string; + index: number; + witness_script?: Uint8Array; + redeemScript?: Uint8Array; + witnessUtxo?: { + script: Uint8Array; + amount: bigint; + }; + nonWitnessUtxo?: Uint8Array; } /** * Create a PSBT with an output `toAddress` and an optional OP_RETURN output. * May add an additional change output. This returns an **unsigned** PSBT encoded * as a Base64 string. - * + * * @param fromAddress The Bitcoin address which is sending to the `toAddress`. * @param toAddress The Bitcoin address which is receiving the BTC. * @param amount The amount of BTC (as satoshis) to send. @@ -42,141 +42,140 @@ export interface Input { * @returns {Promise} The Base64 encoded PSBT. */ export async function createBitcoinPsbt( - fromAddress: string, - toAddress: string, - amount: number, - publicKey?: string, - opReturnData?: string, - confirmationTarget: number = 3, + fromAddress: string, + toAddress: string, + amount: number, + publicKey?: string, + opReturnData?: string, + confirmationTarget: number = 3, ): Promise { - const addressInfo = getAddressInfo(fromAddress); - const network = addressInfo.network; - if (network === "regtest") { - throw new Error("Bitcoin regtest not supported"); - } - - // We need the public key to generate the redeem and witness script to spend the scripts - if (addressInfo.type === (AddressType.p2sh || AddressType.p2wsh)) { - if (!publicKey) { - throw new Error('Public key is required to spend from the selected address type'); + const addressInfo = getAddressInfo(fromAddress); + const network = addressInfo.network; + if (network === "regtest") { + throw new Error("Bitcoin regtest not supported"); } - } - - const esploraClient = new EsploraClient(addressInfo.network); - - // NOTE: esplora only returns the 25 most recent UTXOs - // TODO: change this to use the pagination API and return all UTXOs - const [confirmedUtxos, feeRate] = await Promise.all([ - esploraClient.getAddressUtxos(fromAddress), - esploraClient.getFeeEstimate(confirmationTarget) - ]); - - if (confirmedUtxos.length === 0) { - throw new Error('No confirmed UTXOs'); - } - - // To construct the spending transaction and estimate the fee, we need the transactions for the UTXOs - let possibleInputs: Input[] = []; - - await Promise.all( - confirmedUtxos.map(async (utxo) => { - const hex = await esploraClient.getTransactionHex(utxo.txid); - const transaction = Transaction.fromRaw(Buffer.from(hex, 'hex'), { allowUnknownOutputs: true }); - const input = getInputFromUtxoAndTx(network, utxo, transaction, addressInfo.type, publicKey); - possibleInputs.push(input); - }) - ); - - const outputs: Output[] = [ - { - address: toAddress, - amount: BigInt(amount) + + // We need the public key to generate the redeem and witness script to spend the scripts + if (addressInfo.type === (AddressType.p2sh || AddressType.p2wsh)) { + if (!publicKey) { + throw new Error("Public key is required to spend from the selected address type"); + } + } + + const esploraClient = new EsploraClient(addressInfo.network); + + // NOTE: esplora only returns the 25 most recent UTXOs + // TODO: change this to use the pagination API and return all UTXOs + const [confirmedUtxos, feeRate] = await Promise.all([ + esploraClient.getAddressUtxos(fromAddress), + esploraClient.getFeeEstimate(confirmationTarget), + ]); + + if (confirmedUtxos.length === 0) { + throw new Error("No confirmed UTXOs"); } - ]; - if (opReturnData) { - // Strip 0x prefix from opReturn - if (opReturnData.startsWith('0x')) { - opReturnData = opReturnData.slice(2); + // To construct the spending transaction and estimate the fee, we need the transactions for the UTXOs + let possibleInputs: Input[] = []; + + await Promise.all( + confirmedUtxos.map(async (utxo) => { + const hex = await esploraClient.getTransactionHex(utxo.txid); + const transaction = Transaction.fromRaw(Buffer.from(hex, "hex"), { allowUnknownOutputs: true }); + const input = getInputFromUtxoAndTx(network, utxo, transaction, addressInfo.type, publicKey); + possibleInputs.push(input); + }), + ); + + const outputs: Output[] = [ + { + address: toAddress, + amount: BigInt(amount), + }, + ]; + + if (opReturnData) { + // Strip 0x prefix from opReturn + if (opReturnData.startsWith("0x")) { + opReturnData = opReturnData.slice(2); + } + outputs.push({ + // OP_RETURN https://github.com/paulmillr/scure-btc-signer/issues/26 + script: Script.encode(["RETURN", hex.decode(opReturnData)]), + amount: BigInt(0), + }); } - outputs.push({ - // OP_RETURN https://github.com/paulmillr/scure-btc-signer/issues/26 - script: Script.encode(['RETURN', hex.decode(opReturnData)]), - amount: BigInt(0) - }) - } - - // Outsource UTXO selection to btc-signer - // https://github.com/paulmillr/scure-btc-signer?tab=readme-ov-file#utxo-selection - // default = exactBiggest/accumBiggest creates tx with smallest fees, but it breaks - // big outputs to small ones, which in the end will create a lot of outputs close to dust. - const transaction = selectUTXO(possibleInputs, outputs, 'default', { - changeAddress: fromAddress, // Refund surplus to the payment address - feePerByte: BigInt(Math.ceil(feeRate)), // round up to the nearest integer - bip69: true, // Sort inputs and outputs according to BIP69 - createTx: true, // Create the transaction - network: getBtcNetwork(network), - allowUnknownOutputs: true, // Required for OP_RETURN - allowLegacyWitnessUtxo: true // Required for P2SH-P2WPKH - }); - - if (!transaction || !transaction.tx) { - throw new Error('Failed to create transaction. Do you have enough funds?'); - } - - return base64.encode(transaction.tx.toPSBT(0)); + + // Outsource UTXO selection to btc-signer + // https://github.com/paulmillr/scure-btc-signer?tab=readme-ov-file#utxo-selection + // default = exactBiggest/accumBiggest creates tx with smallest fees, but it breaks + // big outputs to small ones, which in the end will create a lot of outputs close to dust. + const transaction = selectUTXO(possibleInputs, outputs, "default", { + changeAddress: fromAddress, // Refund surplus to the payment address + feePerByte: BigInt(Math.ceil(feeRate)), // round up to the nearest integer + bip69: true, // Sort inputs and outputs according to BIP69 + createTx: true, // Create the transaction + network: getBtcNetwork(network), + allowUnknownOutputs: true, // Required for OP_RETURN + allowLegacyWitnessUtxo: true, // Required for P2SH-P2WPKH + }); + + if (!transaction || !transaction.tx) { + throw new Error("Failed to create transaction. Do you have enough funds?"); + } + + return base64.encode(transaction.tx.toPSBT(0)); } // Using the UTXO and the transaction, we can construct the input for the transaction export function getInputFromUtxoAndTx( - network: BitcoinNetworkName, - utxo: UTXO, - transaction: Transaction, - addressType: AddressType, - publicKey?: string + network: BitcoinNetworkName, + utxo: UTXO, + transaction: Transaction, + addressType: AddressType, + publicKey?: string, ): Input { - // The output containts the necessary details to spend the UTXO based on the script type - // Under the hood, @scure/btc-signer parses the output and extracts the script and amount - const output = transaction.getOutput(utxo.vout); - - // For p2sh, we additionally need the redeem script. This cannot be extracted from the transaction itself - // We only support P2SH-P2WPKH - // TODO: add support for P2WSH - // TODO: add support for P2SH-P2PKH - let redeemScript = {}; - - if (addressType === AddressType.p2sh) { - if (!publicKey) { - throw new Error("Bitcoin P2SH not supported without public key"); + // The output containts the necessary details to spend the UTXO based on the script type + // Under the hood, @scure/btc-signer parses the output and extracts the script and amount + const output = transaction.getOutput(utxo.vout); + + // For p2sh, we additionally need the redeem script. This cannot be extracted from the transaction itself + // We only support P2SH-P2WPKH + // TODO: add support for P2WSH + // TODO: add support for P2SH-P2PKH + let redeemScript = {}; + + if (addressType === AddressType.p2sh) { + if (!publicKey) { + throw new Error("Bitcoin P2SH not supported without public key"); + } + const inner = p2wpkh(Buffer.from(publicKey!, "hex"), getBtcNetwork(network)); + redeemScript = p2sh(inner); } - const inner = p2wpkh(Buffer.from(publicKey!, 'hex'), getBtcNetwork(network)); - redeemScript = p2sh(inner); - } - - // For the redeem and witness script, we need to construct the script mixin - const scriptMixin = { - ...redeemScript - }; - - - const nonWitnessUtxo = { - nonWitnessUtxo: Buffer.from(transaction.hex, 'hex') - }; - const witnessUtxo = { - witnessUtxo: { - script: output.script!, - amount: output.amount! - } - }; - const witnessMixin = transaction.hasWitnesses ? witnessUtxo : nonWitnessUtxo; - - // Construct inputs based on the script type - const input = { - txid: utxo.txid, - index: utxo.vout, - ...scriptMixin, // Maybe adds the redeemScript and/or witnessScript - ...witnessMixin // Adds the witnessUtxo or nonWitnessUtxo - }; - - return input; + + // For the redeem and witness script, we need to construct the script mixin + const scriptMixin = { + ...redeemScript, + }; + + const nonWitnessUtxo = { + nonWitnessUtxo: Buffer.from(transaction.hex, "hex"), + }; + const witnessUtxo = { + witnessUtxo: { + script: output.script!, + amount: output.amount!, + }, + }; + const witnessMixin = transaction.hasWitnesses ? witnessUtxo : nonWitnessUtxo; + + // Construct inputs based on the script type + const input = { + txid: utxo.txid, + index: utxo.vout, + ...scriptMixin, // Maybe adds the redeemScript and/or witnessScript + ...witnessMixin, // Adds the witnessUtxo or nonWitnessUtxo + }; + + return input; } diff --git a/sdk/test/gateway.test.ts b/sdk/test/gateway.test.ts index 4a9f62f8..789aa307 100644 --- a/sdk/test/gateway.test.ts +++ b/sdk/test/gateway.test.ts @@ -1,12 +1,24 @@ -import { assert, describe, it } from "vitest"; +import { assert, describe, expect, it } from "vitest"; import { GatewaySDK } from "../src/gateway"; import { MAINNET_GATEWAY_BASE_URL } from "../src/gateway/client"; import { SYMBOL_LOOKUP } from "../src/gateway/tokens"; +import { Chain, ChainId } from "../src/gateway/types"; import { ZeroAddress } from "ethers"; import nock from "nock"; import * as bitcoin from "bitcoinjs-lib"; describe("Gateway Tests", () => { + it("should get chains", async () => { + const gatewaySDK = new GatewaySDK("bob"); + assert.deepEqual(gatewaySDK.getChains(), Object.values(Chain)); + }); + + it("should reject invalid chain", async () => { + expect(() => { + new GatewaySDK("bob-testnet"); + }).toThrowError("Invalid chain"); + }); + it("should get quote", async () => { const gatewaySDK = new GatewaySDK("mainnet"); @@ -21,7 +33,7 @@ describe("Gateway Tests", () => { }; nock(`${MAINNET_GATEWAY_BASE_URL}`) - .get(`/quote/${SYMBOL_LOOKUP["tbtc"].bob}/1000`) + .get(`/quote/${SYMBOL_LOOKUP[ChainId.BOB]["tbtc"].address}?satoshis=1000`) .times(4) .reply(200, mockQuote); @@ -45,22 +57,55 @@ describe("Gateway Tests", () => { }), mockQuote); assert.deepEqual(await gatewaySDK.getQuote({ toChain: "BOB", - toToken: SYMBOL_LOOKUP["tbtc"].bob, + toToken: SYMBOL_LOOKUP[ChainId.BOB]["tbtc"].address, toUserAddress: ZeroAddress, amount: 1000, }), mockQuote); // get the total available without amount nock(`${MAINNET_GATEWAY_BASE_URL}`) - .get(`/quote/${SYMBOL_LOOKUP["tbtc"].bob}/`) + .get(`/quote/${SYMBOL_LOOKUP[ChainId.BOB]["tbtc"].address}`) .reply(200, mockQuote); assert.deepEqual(await gatewaySDK.getQuote({ toChain: "BOB", - toToken: SYMBOL_LOOKUP["tbtc"].bob, + toToken: SYMBOL_LOOKUP[ChainId.BOB]["tbtc"].address, toUserAddress: ZeroAddress, }), mockQuote); }); + it("should throw error on invalid token", async () => { + const gatewaySDK = new GatewaySDK("mainnet"); + await expect(async () => { + await gatewaySDK.getQuote({ + toChain: "BOB", + toToken: "unknownToken", + toUserAddress: ZeroAddress, + amount: 1000, + }); + }).rejects.toThrowError("Unknown output token"); + }); + + it("should reject invalid network", async () => { + const gatewaySDK = new GatewaySDK("testnet"); + await expect(async () => { + await gatewaySDK.getQuote({ + toChain: "BOB", + toToken: "tbtc", + toUserAddress: ZeroAddress, + amount: 1000, + }); + }).rejects.toThrowError("Invalid output chain"); + + await expect(async () => { + await gatewaySDK.getQuote({ + toChain: "unknownChain", + toToken: "tbtc", + toUserAddress: ZeroAddress, + amount: 1000, + }); + }).rejects.toThrowError("Invalid output chain"); + }); + it("should start order", async () => { const gatewaySDK = new GatewaySDK("bob"); @@ -87,6 +132,7 @@ describe("Gateway Tests", () => { toUserAddress: ZeroAddress, amount: 1000, fromChain: "Bitcoin", + fromToken: "BTC", fromUserAddress: "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq", }); @@ -100,4 +146,54 @@ describe("Gateway Tests", () => { ]) ); }); + + it("should get strategies", async () => { + nock(`${MAINNET_GATEWAY_BASE_URL}`) + .get(`/strategies`) + .reply(200, [{ + strategyAddress: ZeroAddress, + inputTokenAddress: SYMBOL_LOOKUP[ChainId.BOB]["tbtc"].address, + name: "", + slug: "", + type: "staking" + }]); + nock(`${MAINNET_GATEWAY_BASE_URL}`) + .get(`/quote/${SYMBOL_LOOKUP[ChainId.BOB]["tbtc"].address}?satoshis=1000&strategy=${ZeroAddress}`) + .times(4) + .reply(200, { + gatewayAddress: ZeroAddress, + dustThreshold: 1000, + satoshis: 1000, + fee: 10, + bitcoinAddress: "", + txProofDifficultyFactor: 3, + strategyAddress: ZeroAddress, + }); + + const gatewaySDK = new GatewaySDK("bob"); + const strategies = await gatewaySDK.getStrategies(); + + assert.lengthOf(strategies, 1); + assert.isDefined(strategies[0].inputToken); + assert.isNull(strategies[0].outputToken); + + const strategy = strategies[0]; + await gatewaySDK.getQuote({ + toUserAddress: ZeroAddress, + amount: 1000, + + toChain: strategy.chain.chainId, + toToken: strategy.inputToken.symbol, + strategyAddress: strategy.address, + }); + }); + + it("should get tokens", async () => { + nock(`${MAINNET_GATEWAY_BASE_URL}`) + .get(`/tokens?includeStrategies=false`) + .reply(200, [ZeroAddress]); + + const gatewaySDK = new GatewaySDK("bob"); + assert.deepEqual(await gatewaySDK.getTokenAddresses(false), [ZeroAddress]); + }); });