diff --git a/package-lock.json b/package-lock.json index 549d480..9f8576d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,10 +17,12 @@ "axios": "^1.4.0", "class-validator": "^0.14.0", "dotenv": "^16.0.3", + "ergo-lib-wasm-nodejs": "^0.26.0", "graphql": "^15.8.0", "graphql-depth-limit": "^1.1.0", "graphql-type-json": "^0.3.2", "ioredis": "5.3.1", + "json-bigint": "^1.0.0", "lodash": "^4.17.21", "pg": "^8.11.0", "reflect-metadata": "^0.1.13", @@ -31,6 +33,7 @@ "@types/graphql-depth-limit": "^1.1.3", "@types/ioredis": "^4.28.10", "@types/jest": "^28.1.8", + "@types/json-bigint": "^1.0.4", "@types/lodash": "^4.14.191", "@types/node": "^18.13.0", "@types/validator": "^13.7.12", @@ -1816,6 +1819,12 @@ "pretty-format": "^28.0.0" } }, + "node_modules/@types/json-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@types/json-bigint/-/json-bigint-1.0.4.tgz", + "integrity": "sha512-ydHooXLbOmxBbubnA7Eh+RpBzuaIiQjh8WGJYQB50JFGFrdxW7JzVlyEV7fAXw0T2sqJ1ysTneJbiyNLqZRAag==", + "dev": true + }, "node_modules/@types/json-schema": { "version": "7.0.11", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", @@ -2689,6 +2698,14 @@ } ] }, + "node_modules/bignumber.js": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz", + "integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==", + "engines": { + "node": "*" + } + }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -3364,6 +3381,11 @@ "node": ">= 0.8" } }, + "node_modules/ergo-lib-wasm-nodejs": { + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/ergo-lib-wasm-nodejs/-/ergo-lib-wasm-nodejs-0.26.0.tgz", + "integrity": "sha512-sG+MOwYKrCgcUbCHwnCOvHHS5wxEkaO/G8zUxlMiX6cSAdN06ddVIGflyqzebKe3z6OO1cN9tfMX0W7fJnzKHg==" + }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -5298,6 +5320,14 @@ "node": ">=4" } }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", @@ -8958,6 +8988,12 @@ "pretty-format": "^28.0.0" } }, + "@types/json-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@types/json-bigint/-/json-bigint-1.0.4.tgz", + "integrity": "sha512-ydHooXLbOmxBbubnA7Eh+RpBzuaIiQjh8WGJYQB50JFGFrdxW7JzVlyEV7fAXw0T2sqJ1ysTneJbiyNLqZRAag==", + "dev": true + }, "@types/json-schema": { "version": "7.0.11", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", @@ -9589,6 +9625,11 @@ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" }, + "bignumber.js": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz", + "integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==" + }, "binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -10085,6 +10126,11 @@ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" }, + "ergo-lib-wasm-nodejs": { + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/ergo-lib-wasm-nodejs/-/ergo-lib-wasm-nodejs-0.26.0.tgz", + "integrity": "sha512-sG+MOwYKrCgcUbCHwnCOvHHS5wxEkaO/G8zUxlMiX6cSAdN06ddVIGflyqzebKe3z6OO1cN9tfMX0W7fJnzKHg==" + }, "error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -11512,6 +11558,14 @@ "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", "dev": true }, + "json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "requires": { + "bignumber.js": "^9.0.0" + } + }, "json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", diff --git a/package.json b/package.json index d242c99..fbaa5b0 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "@types/graphql-depth-limit": "^1.1.3", "@types/ioredis": "^4.28.10", "@types/jest": "^28.1.8", + "@types/json-bigint": "^1.0.4", "@types/lodash": "^4.14.191", "@types/node": "^18.13.0", "@types/validator": "^13.7.12", @@ -47,10 +48,12 @@ "axios": "^1.4.0", "class-validator": "^0.14.0", "dotenv": "^16.0.3", + "ergo-lib-wasm-nodejs": "^0.26.0", "graphql": "^15.8.0", "graphql-depth-limit": "^1.1.0", "graphql-type-json": "^0.3.2", "ioredis": "5.3.1", + "json-bigint": "^1.0.0", "lodash": "^4.17.21", "pg": "^8.11.0", "reflect-metadata": "^0.1.13", diff --git a/src/graphql/input-types/transaction.ts b/src/graphql/input-types/transaction.ts index 0a1e65a..d62eb61 100644 --- a/src/graphql/input-types/transaction.ts +++ b/src/graphql/input-types/transaction.ts @@ -20,3 +20,15 @@ export class SignedTransactionInput { @Field({ nullable: true }) size?: number; } + +@InputType("UnsignedTransaction") +export class UnsignedTransactionInput { + @Field(() => String) + unsignedTransaction!: string; + + @Field(() => [String]) + inputBoxes!: string[]; + + @Field(() => [String]) + dataInputBoxes!: string[]; +} diff --git a/src/graphql/resolvers/transaction-resolver.ts b/src/graphql/resolvers/transaction-resolver.ts index 72ea347..a532c3b 100644 --- a/src/graphql/resolvers/transaction-resolver.ts +++ b/src/graphql/resolvers/transaction-resolver.ts @@ -1,10 +1,24 @@ import { GraphQLResolveInfo } from "graphql"; -import { Args, ArgsType, Ctx, Field, Info, Int, Query, Resolver } from "type-graphql"; +import { + Arg, + Args, + ArgsType, + Ctx, + Field, + Info, + Int, + Mutation, + Query, + Resolver +} from "type-graphql"; import { Transaction } from "../objects/transaction"; import { removeUndefined } from "../../utils"; import { GraphQLContext } from "../context-type"; import { PaginationArguments } from "./pagination-arguments"; import { ArrayMaxSize } from "class-validator"; +import { UnsignedTransactionInput } from "../input-types"; +import * as wasm from "ergo-lib-wasm-nodejs"; +import { nodeService } from "../../services"; @ArgsType() class TransactionArguments { @@ -72,4 +86,24 @@ export class TransactionResolver { take }); } + + @Mutation(() => String) + async reduceTransaction(@Arg("transaction") transaction: UnsignedTransactionInput) { + try { + const ctx = await nodeService.getStateContext(); + const tx = wasm.UnsignedTransaction.from_json(transaction.unsignedTransaction); + const ergoBoxes = wasm.ErgoBoxes.from_boxes_json(transaction.inputBoxes); + const dataInputBoxes = wasm.ErgoBoxes.from_boxes_json([]); + const reducedTx = wasm.ReducedTransaction.from_unsigned_tx( + tx, + ergoBoxes, + dataInputBoxes, + ctx + ); + return reducedTx.sigma_serialize_bytes().toString(); + } catch (e: any) { + console.error(e); + throw new Error(`Failed to reduce transaction! ${e}`); + } + } } diff --git a/src/graphql/schema.graphql b/src/graphql/schema.graphql index dc403cc..1efdd23 100644 --- a/src/graphql/schema.graphql +++ b/src/graphql/schema.graphql @@ -218,6 +218,7 @@ type Mempool { type Mutation { checkTransaction(signedTransaction: SignedTransaction!): String! + reduceTransaction(transaction: UnsignedTransaction!): String! submitTransaction(signedTransaction: SignedTransaction!): String! } @@ -369,3 +370,9 @@ type UnconfirmedTransaction implements ITransaction { timestamp: String! transactionId: String! } + +input UnsignedTransaction { + dataInputBoxes: [String!]! + inputBoxes: [String!]! + unsignedTransaction: String! +} diff --git a/src/services/node-service.ts b/src/services/node-service.ts index ef0525e..3983d6e 100644 --- a/src/services/node-service.ts +++ b/src/services/node-service.ts @@ -1,5 +1,7 @@ import axios from "axios"; import { SignedTransactionInput } from "../graphql/input-types"; +import * as wasm from "ergo-lib-wasm-nodejs"; +import { JsonBI } from "../utils"; const HTTP_PREFIX_PATTERN = /^http(s?):\/\//; @@ -30,6 +32,23 @@ class NodeService { public getNodeInfo() { return axios.get(this._baseUrl + "/info"); } + + public getStateContext = async () => { + try { + const lastHeaderResponse = await axios.get(this._baseUrl + "/blocks/lastHeaders/10"); + const lastBlocks = lastHeaderResponse.data as any[]; + const lastBlocksStrings = lastBlocks.map((header) => JsonBI.stringify(header)); + const lastBlocksHeaders = wasm.BlockHeaders.from_json(lastBlocksStrings); + const lastBlockPreHeader = wasm.PreHeader.from_block_header(lastBlocksHeaders.get(0)); + + const stateContext = new wasm.ErgoStateContext(lastBlockPreHeader, lastBlocksHeaders); + + return stateContext; + } catch (e) { + console.error(e); + throw new Error("Failed to get state context"); + } + }; } export const nodeService = new NodeService(); diff --git a/src/utils.ts b/src/utils.ts index 18b5668..6d12b3f 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,3 +1,5 @@ +import JSONBigInt from "json-bigint"; + export function removeUndefined(value: Record) { const result: Record = {}; for (const key in value) { @@ -9,3 +11,8 @@ export function removeUndefined(value: Record) { return result; } + +export const JsonBI = JSONBigInt({ + useNativeBigInt: true, + alwaysParseAsBig: true +});