diff --git a/packages-cache/@emurgo-cardano-serialization-lib-nodejs-5.0.0-rc.0.tgz b/packages-cache/@emurgo-cardano-serialization-lib-nodejs-5.0.0-rc.0.tgz new file mode 100644 index 00000000..bec4194c Binary files /dev/null and b/packages-cache/@emurgo-cardano-serialization-lib-nodejs-5.0.0-rc.0.tgz differ diff --git a/packages-cache/@types-temp-write-4.0.0.tgz b/packages-cache/@types-temp-write-4.0.0.tgz new file mode 100644 index 00000000..4e899091 Binary files /dev/null and b/packages-cache/@types-temp-write-4.0.0.tgz differ diff --git a/packages-cache/temp-dir-1.0.0.tgz b/packages-cache/temp-dir-1.0.0.tgz new file mode 100644 index 00000000..3458eb0e Binary files /dev/null and b/packages-cache/temp-dir-1.0.0.tgz differ diff --git a/packages-cache/temp-write-4.0.0.tgz b/packages-cache/temp-write-4.0.0.tgz new file mode 100644 index 00000000..eda94c15 Binary files /dev/null and b/packages-cache/temp-write-4.0.0.tgz differ diff --git a/packages/api-cardano-db-hasura/package.json b/packages/api-cardano-db-hasura/package.json index 3b2ab965..11a00e8f 100644 --- a/packages/api-cardano-db-hasura/package.json +++ b/packages/api-cardano-db-hasura/package.json @@ -33,6 +33,7 @@ ], "dependencies": { "@cardano-graphql/util": "3.1.0", + "@emurgo/cardano-serialization-lib-nodejs": "^5.0.0-rc.0", "@graphql-tools/delegate": "^6.0.10", "@graphql-tools/schema": "^6.0.9", "@graphql-tools/wrap": "^6.0.9", @@ -51,6 +52,7 @@ "pg": "^8.5.1", "pg-listen": "^1.6.0", "set-interval-async": "^1.0.33", + "temp-write": "^4.0.0", "ts-log": "^2.2.3" }, "devDependencies": { @@ -62,6 +64,7 @@ "@types/node": "^14.0.13", "@types/pg": "^7.14.4", "@types/set-interval-async": "^1.0.0", + "@types/temp-write": "^4.0.0", "shx": "^0.3.2", "typescript": "^3.9.5" } diff --git a/packages/api-cardano-db-hasura/schema.graphql b/packages/api-cardano-db-hasura/schema.graphql index 47479d04..16fe505b 100644 --- a/packages/api-cardano-db-hasura/schema.graphql +++ b/packages/api-cardano-db-hasura/schema.graphql @@ -1,4 +1,5 @@ schema { + mutation: Mutation query: Query } @@ -16,6 +17,11 @@ scalar Timestamp scalar URL scalar VRFVerificationKey +type Mutation { + # Submit a signed transaction to the network + submitTransaction (transaction: String!): TransactionSubmitResponse! +} + type Query { activeStake ( limit: Int @@ -1044,6 +1050,10 @@ type TransactionOutput_sum_fields { value: String } +type TransactionSubmitResponse { + hash: String! +} + type Block { # Genesis block does not belong to the 0th epoch, therefore it could be null epoch: Epoch diff --git a/packages/api-cardano-db-hasura/src/CardanoCli.ts b/packages/api-cardano-db-hasura/src/CardanoCli.ts index 2e43d42d..1eab2fb6 100644 --- a/packages/api-cardano-db-hasura/src/CardanoCli.ts +++ b/packages/api-cardano-db-hasura/src/CardanoCli.ts @@ -2,7 +2,7 @@ import { exec } from 'child_process' import { Genesis, ShelleyProtocolParams } from './graphql_types' import { Config } from './Config' import { LedgerState } from './CardanoNodeClient' -import { knownEras } from '@cardano-graphql/util' +import { knownEras, capitalizeFirstChar } from '@cardano-graphql/util' export interface CardanoCliTip { blockNo: number, @@ -14,14 +14,15 @@ export type ProtocolParams = ShelleyProtocolParams const isEraMismatch = (errorMessage: string, era: string): boolean => { return errorMessage.includes('EraMismatch') || errorMessage.includes( - `The attempted local state query does not support the ${era.charAt(0).toUpperCase().concat(era.slice(1))} protocol` + `The attempted local state query does not support the ${capitalizeFirstChar(era)} protocol` ) } export interface CardanoCli { getLedgerState(): Promise, getProtocolParams(): Promise, - getTip(): Promise + getTip(): Promise, + submitTransaction(filePath: string): Promise } export function createCardanoCli ( @@ -75,6 +76,19 @@ export function createCardanoCli ( withEraFlag: true } ), - getTip: () => query('tip') + getTip: () => query('tip'), + submitTransaction: (filePath) => { + return new Promise((resolve, reject) => { + exec( + `${cardanoCliPath} transaction submit --tx-file ${filePath} ${networkArg}`, + (error, _stdout, stderr) => { + if (error !== null || stderr.toString() !== '') { + return reject(new Error(stderr.toString())) + } + return resolve() + } + ) + }) + } } } diff --git a/packages/api-cardano-db-hasura/src/CardanoNodeClient.ts b/packages/api-cardano-db-hasura/src/CardanoNodeClient.ts index 4eff4f0c..25b451b8 100644 --- a/packages/api-cardano-db-hasura/src/CardanoNodeClient.ts +++ b/packages/api-cardano-db-hasura/src/CardanoNodeClient.ts @@ -1,9 +1,11 @@ import { CardanoCli } from './CardanoCli' import fs from 'fs-extra' -import { AssetSupply } from './graphql_types' +import { AssetSupply, Transaction } from './graphql_types' import pRetry from 'p-retry' -import util, { DataFetcher } from '@cardano-graphql/util' +import util, { DataFetcher, knownEras } from '@cardano-graphql/util' +import tempWrite from 'temp-write' import { dummyLogger, Logger } from 'ts-log' +import { getHashOfSignedTransaction } from './util' export type LedgerState = { accountState: { @@ -15,6 +17,23 @@ export type LedgerState = { } } +const fileTypeFromEra = (era: string) => { + switch (era) { + case 'mary' : + return 'Tx MaryEra' + case 'allegra' : + return 'Tx AllegraEra' + case 'shelley' : + return 'TxSignedShelley' + default : + throw new Error(`Transaction not submitted. ${era} era not supported.`) + } +} + +const isEraMismatch = (errorMessage: string): boolean => + errorMessage.includes('DecoderErrorDeserialiseFailure') || + errorMessage.includes('The era of the node and the tx do not match') + export class CardanoNodeClient { readonly networkParams: string[] public adaCirculatingSupply: AssetSupply['circulating'] @@ -79,4 +98,26 @@ export class CardanoNodeClient { public async shutdown () { await this.ledgerStateFetcher.shutdown() } + + public async submitTransaction (transaction: string): Promise { + for (const era of knownEras) { + const filePath = await tempWrite(`{ + "type": "${fileTypeFromEra(era)}", + "description": "", + "cborHex": "${transaction}" + }`) + const hash = getHashOfSignedTransaction(transaction) + try { + await this.cardanoCli.submitTransaction(filePath) + this.logger.info('submitTransaction', { module: 'CardanoNodeClient', hash: hash }) + return hash + } catch (error) { + if (!isEraMismatch(error.message)) { + throw error + } + } finally { + await fs.unlink(filePath) + } + } + } } diff --git a/packages/api-cardano-db-hasura/src/example_queries/transactions/submitTransaction.graphql b/packages/api-cardano-db-hasura/src/example_queries/transactions/submitTransaction.graphql new file mode 100644 index 00000000..17820fcd --- /dev/null +++ b/packages/api-cardano-db-hasura/src/example_queries/transactions/submitTransaction.graphql @@ -0,0 +1,5 @@ +mutation submitTransaction( + $transaction: String! +) { + submitTransaction(transaction: $transaction) +} diff --git a/packages/api-cardano-db-hasura/src/executableSchema.ts b/packages/api-cardano-db-hasura/src/executableSchema.ts index 07f9301a..92251c9f 100644 --- a/packages/api-cardano-db-hasura/src/executableSchema.ts +++ b/packages/api-cardano-db-hasura/src/executableSchema.ts @@ -45,6 +45,13 @@ export async function buildSchema ( } return makeExecutableSchema({ resolvers: Object.assign({}, scalarResolvers, { + Mutation: { + submitTransaction: async (_root, args) => { + await throwIfNotInCurrentEra('submitTransaction') + const hash = await cardanoNodeClient.submitTransaction(args.transaction) + return { hash } + } + }, PaymentAddress: { summary: async (parent, args) => { try { diff --git a/packages/api-cardano-db-hasura/src/util.ts b/packages/api-cardano-db-hasura/src/util.ts index be658143..72c6b7b0 100644 --- a/packages/api-cardano-db-hasura/src/util.ts +++ b/packages/api-cardano-db-hasura/src/util.ts @@ -1,3 +1,4 @@ +import CardanoWasm from '@emurgo/cardano-serialization-lib-nodejs' import { Config } from './Config' import fs from 'fs-extra' import path from 'path' @@ -9,3 +10,10 @@ export async function readSecrets (rootDir: string): Promise word.charAt(0).toUpperCase().concat(word.slice(1)) diff --git a/yarn.lock b/yarn.lock index 18852405..311abc2a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -574,6 +574,11 @@ exec-sh "^0.3.2" minimist "^1.2.0" +"@emurgo/cardano-serialization-lib-nodejs@^5.0.0-rc.0": + version "5.0.0-rc.0" + resolved "https://registry.yarnpkg.com/@emurgo/cardano-serialization-lib-nodejs/-/cardano-serialization-lib-nodejs-5.0.0-rc.0.tgz#da6e7679ee83e467adfc78c2789b2a5129f7f6b1" + integrity sha512-CVd5YHVIsxiokJTYnyZunzr8jaeuEMVb83Jjt4j0OTNJJfp3ReV0ZIvGsoGJUP2K+07nT8xWnHDEDrMH97ZIkQ== + "@graphql-codegen/cli@^1.15.2": version "1.16.2" resolved "https://registry.yarnpkg.com/@graphql-codegen/cli/-/cli-1.16.2.tgz#ff92e5e6813a404616d7504500be26ff88b7092d" @@ -1699,6 +1704,13 @@ resolved "https://registry.yarnpkg.com/@types/strip-json-comments/-/strip-json-comments-0.0.30.tgz#9aa30c04db212a9a0649d6ae6fd50accc40748a1" integrity sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ== +"@types/temp-write@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@types/temp-write/-/temp-write-4.0.0.tgz#983237bb6dbcac883137dba8189b8c6177e8a04d" + integrity sha512-BNcDNG/ujXmzR49TeVxcWMbglOO55YQbe1ij3LE6WoZdLtZvclSEl5GAmmlfsr0Y5kM9mLIE1wk3r3pzQz62gQ== + dependencies: + temp-write "*" + "@types/through@*": version "0.0.30" resolved "https://registry.yarnpkg.com/@types/through/-/through-0.0.30.tgz#e0e42ce77e897bd6aead6f6ea62aeb135b8a3895" @@ -8676,6 +8688,22 @@ tdigest@^0.1.1: dependencies: bintrees "1.0.1" +temp-dir@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/temp-dir/-/temp-dir-1.0.0.tgz#0a7c0ea26d3a39afa7e0ebea9c1fc0bc4daa011d" + integrity sha1-CnwOom06Oa+n4OvqnB/AvE2qAR0= + +temp-write@*, temp-write@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/temp-write/-/temp-write-4.0.0.tgz#cd2e0825fc826ae72d201dc26eef3bf7e6fc9320" + integrity sha512-HIeWmj77uOOHb0QX7siN3OtwV3CTntquin6TNVg6SHOqCP3hYKmox90eeFOGaY1MqJ9WYDDjkyZrW6qS5AWpbw== + dependencies: + graceful-fs "^4.1.15" + is-stream "^2.0.0" + make-dir "^3.0.0" + temp-dir "^1.0.0" + uuid "^3.3.2" + terminal-link@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/terminal-link/-/terminal-link-2.1.1.tgz#14a64a27ab3c0df933ea546fba55f2d078edc994"