diff --git a/README.md b/README.md index 38ee5e7d7cc..54263ffacbc 100644 --- a/README.md +++ b/README.md @@ -137,6 +137,18 @@ for you. nix run .#config-update ``` +## Get CBOR representation of an on chain transaction + +Once we have a [running network](packages/cardano-services/README.md#production) synced at least up to the block +containing the transaction we are interested in, issue following command to get the CBOR representation of the +transaction. + +``` +yarn tx-cbor +``` + +This works regardless of the local ports configuration through environment variables. + ## Attic Previously supported features, no longer supported, but packed with a reference branch. diff --git a/package.json b/package.json index 78d6aa88cc3..67521f66fdc 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,8 @@ "test": "yarn workspaces foreach -v run test", "test:build:verify": "yarn workspaces foreach -v run test:build:verify", "test:e2e": "yarn workspaces foreach -v run test:e2e", - "test:debug": "DEBUG=true yarn workspaces foreach -v run test" + "test:debug": "DEBUG=true yarn workspaces foreach -v run test", + "tx-cbor": "tsx packages/cardano-services/scripts/tx-cbor.js" }, "repository": { "type": "git", diff --git a/packages/cardano-services/scripts/tx-cbor.ts b/packages/cardano-services/scripts/tx-cbor.ts new file mode 100644 index 00000000000..f1bd59b608c --- /dev/null +++ b/packages/cardano-services/scripts/tx-cbor.ts @@ -0,0 +1,105 @@ +import { Pool } from 'pg'; +import { readFileSync } from 'fs'; +import { spawnSync } from 'child_process'; +import WebSocket from 'ws'; + +interface Tx { + cbor: string; + id: string; +} + +const txId = process.argv[2]; + +const normalizeError = (status: number | null, stderr: string) => + [status || 1, stderr || 'Unknown error\n', ''] as const; + +const inside = async () => { + const db = new Pool({ + database: readFileSync(process.env.POSTGRES_DB_FILE_DB_SYNC!).toString(), + host: 'postgres', + password: readFileSync(process.env.POSTGRES_PASSWORD_FILE_DB_SYNC!).toString(), + user: readFileSync(process.env.POSTGRES_USER_FILE_DB_SYNC!).toString() + }); + + const query = `\ +SELECT ENCODE(b2.hash, 'hex') AS id, b2.slot_no::INTEGER AS slot FROM tx +JOIN block b1 ON block_id = b1.id +JOIN block b2 ON b1.previous_id = b2.id +WHERE tx.hash = $1`; + + const { rows } = await db.query(query, [Buffer.from(txId, 'hex')]); + const [prevBlock] = rows; + + await db.end(); + + if (!prevBlock) return [1, `Unknown transaction id ${txId}\n`, ''] as const; + + const cbor = await new Promise((resolve) => { + const client = new WebSocket(process.env.OGMIOS_URL!); + let request = 0; + + const rpc = (method: string, params: unknown) => + client.send(JSON.stringify({ id: ++request, jsonrpc: '2.0', method, params })); + + client.on('open', () => rpc('findIntersection', { points: [prevBlock] })); + + client.on('message', (msg) => { + const { result } = JSON.parse(msg.toString()) as { result: { block: { transactions: Tx[] } } }; + let tx: Tx | undefined; + + if ( + result && + result.block && + result.block.transactions && + (tx = result.block.transactions.find((t) => t.id === txId)) + ) { + client.on('close', () => resolve(tx!.cbor)); + client.close(); + } else rpc('nextBlock', {}); + }); + }); + + return [0, '', `${cbor}\n`] as const; +}; + +const outside = async () => { + if (!txId) return [1, 'Missing input transaction id\n', ''] as const; + + let { status, stderr, stdout } = spawnSync('docker', ['ps'], { encoding: 'utf-8' }); + + if (status || stderr) return normalizeError(status, stderr); + + const container = [ + 'cardano-services-mainnet-provider-server-1', + 'cardano-services-preprod-provider-server-1', + 'cardano-services-preview-provider-server-1', + 'cardano-services-sanchonet-provider-server-1', + 'local-network-e2e-provider-server-1' + ].find((name) => stdout.includes(name)); + + if (!container) return [1, "Can't find any valid container\n", ''] as const; + + ({ status, stderr, stdout } = spawnSync( + 'docker', + ['container', 'exec', '-i', container, 'bash', '-c', `cd /app ; INSIDE_THE_CONTAINER=true yarn tx-cbor ${txId}`], + { encoding: 'utf-8' } + )); + + if (status || stderr) return normalizeError(status, stderr); + + return [0, '', stdout] as const; +}; + +(process.env.INSIDE_THE_CONTAINER ? inside() : outside()) + .then(([status, stderr, stdout]) => { + if (status) { + process.stderr.write(stderr); + // eslint-disable-next-line unicorn/no-process-exit + process.exit(status); + } + + process.stdout.write(stdout); + }) + .catch((error) => { + throw error; + }); diff --git a/packages/cardano-services/src/ChainHistory/DbSyncChainHistory/ChainHistoryBuilder.ts b/packages/cardano-services/src/ChainHistory/DbSyncChainHistory/ChainHistoryBuilder.ts index b90c6b799af..422556c2016 100644 --- a/packages/cardano-services/src/ChainHistory/DbSyncChainHistory/ChainHistoryBuilder.ts +++ b/packages/cardano-services/src/ChainHistory/DbSyncChainHistory/ChainHistoryBuilder.ts @@ -1,3 +1,5 @@ +// cSpell:ignore descr + import * as Queries from './queries'; import { AuthorizeCommitteeHotCertModel, @@ -261,7 +263,10 @@ export class ChainHistoryBuilder { if (result.rows.length === 0) return []; const txOutIds = result.rows.flatMap((txOut) => BigInt(txOut.id)); - const multiAssets = await this.queryMultiAssetsByTxOut(txOutIds); + // In case of collateralReturn requests (collateral = true) assets in the output can't be read as for regular outputs: + // db-sync stores assets from collateral outputs in collateral_tx_out.multi_assets_descr column rather than in + // ma_tx_out table like for regular outputs. To have a complete collateralReturn, given column should be read and parsed. + const multiAssets = collateral ? new Map() : await this.queryMultiAssetsByTxOut(txOutIds); const referenceScripts = await this.queryReferenceScriptsByTxOut(result.rows); return result.rows.map((txOut) => diff --git a/packages/cardano-services/src/ChainHistory/DbSyncChainHistory/DbSyncChainHistoryProvider.ts b/packages/cardano-services/src/ChainHistory/DbSyncChainHistory/DbSyncChainHistoryProvider.ts index 92718a4eb53..8625fd44011 100644 --- a/packages/cardano-services/src/ChainHistory/DbSyncChainHistory/DbSyncChainHistoryProvider.ts +++ b/packages/cardano-services/src/ChainHistory/DbSyncChainHistory/DbSyncChainHistoryProvider.ts @@ -136,12 +136,10 @@ export class DbSyncChainHistoryProvider extends DbSyncProvider() implements Chai return txResults.rows.map((tx) => { const txId = tx.id.toString('hex') as unknown as Cardano.TransactionId; - const txInputs = orderBy(inputs.filter((input) => input.txInputId === txId).map(mapTxIn), ['index']); - const txCollaterals = orderBy(collaterals.filter((col) => col.txInputId === txId).map(mapTxIn), ['index']); + const txInputs = inputs.filter((input) => input.txInputId === txId).map(mapTxIn); + const txCollaterals = collaterals.filter((col) => col.txInputId === txId).map(mapTxIn); const txOutputs = orderBy(outputs.filter((output) => output.txId === txId).map(mapTxOut), ['index']); - const txCollateralOutputs = orderBy(collateralOutputs.filter((output) => output.txId === txId).map(mapTxOut), [ - 'index' - ]); + const txCollateralOutputs = collateralOutputs.filter((output) => output.txId === txId).map(mapTxOut); const inputSource: Cardano.InputSource = tx.valid_contract ? Cardano.InputSource.inputs : Cardano.InputSource.collaterals; diff --git a/packages/cardano-services/src/ChainHistory/DbSyncChainHistory/mappers.ts b/packages/cardano-services/src/ChainHistory/DbSyncChainHistory/mappers.ts index 2a30d72b83a..1b9c5d1b37d 100644 --- a/packages/cardano-services/src/ChainHistory/DbSyncChainHistory/mappers.ts +++ b/packages/cardano-services/src/ChainHistory/DbSyncChainHistory/mappers.ts @@ -5,6 +5,7 @@ import { BlockOutputModel, CertificateModel, MultiAssetModel, + PoolRegisterCertModel, ProtocolParametersUpdateModel, RedeemerModel, ScriptModel, @@ -191,6 +192,17 @@ const mapDrepDelegation = ({ __typename: 'AlwaysAbstain' }; +const mapPoolParameters = (certModel: WithCertType): Cardano.PoolParameters => ({ + cost: BigInt(certModel.fixed_cost), + id: certModel.pool_id as unknown as Cardano.PoolId, + margin: Cardano.FractionUtils.toFraction(certModel.margin), + owners: [], + pledge: BigInt(certModel.pledge), + relays: [], + rewardAccount: certModel.reward_account as Cardano.RewardAccount, + vrf: certModel.vrf_key_hash.toString('hex') as Cardano.VrfVkHex +}); + // eslint-disable-next-line complexity export const mapCertificate = ( certModel: WithCertType @@ -209,7 +221,7 @@ export const mapCertificate = ( __typename: Cardano.CertificateType.PoolRegistration, cert_index: certModel.cert_index, deposit: BigInt(certModel.deposit), - poolParameters: null as unknown as Cardano.PoolParameters + poolParameters: mapPoolParameters(certModel) } as WithCertIndex; if (isMirCertModel(certModel)) { @@ -231,12 +243,7 @@ export const mapCertificate = ( : Cardano.CertificateType.Unregistration, cert_index: certModel.cert_index, deposit: BigInt(certModel.deposit), - stakeCredential: { - hash: Cardano.RewardAccount.toHash( - Cardano.RewardAccount(certModel.address) - ) as unknown as Crypto.Hash28ByteBase16, - type: Cardano.CredentialType.KeyHash - } + stakeCredential: Cardano.Address.fromBech32(certModel.address).asReward()!.getPaymentCredential() } as WithCertIndex; if (isDelegationCertModel(certModel)) @@ -244,12 +251,7 @@ export const mapCertificate = ( __typename: Cardano.CertificateType.StakeDelegation, cert_index: certModel.cert_index, poolId: certModel.pool_id as unknown as Cardano.PoolId, - stakeCredential: { - hash: Cardano.RewardAccount.toHash( - Cardano.RewardAccount(certModel.address) - ) as unknown as Crypto.Hash28ByteBase16, - type: Cardano.CredentialType.KeyHash - } + stakeCredential: Cardano.Address.fromBech32(certModel.address).asReward()!.getPaymentCredential() } as WithCertIndex; if (isDrepRegistrationCertModel(certModel)) @@ -292,12 +294,7 @@ export const mapCertificate = ( __typename: Cardano.CertificateType.VoteDelegation, cert_index: certModel.cert_index, dRep: mapDrepDelegation(certModel), - stakeCredential: { - hash: Cardano.RewardAccount.toHash( - Cardano.RewardAccount(certModel.address) - ) as unknown as Crypto.Hash28ByteBase16, - type: Cardano.CredentialType.KeyHash - } + stakeCredential: Cardano.Address.fromBech32(certModel.address).asReward()!.getPaymentCredential() }; if (isVoteRegistrationDelegationCertModel(certModel)) @@ -306,12 +303,7 @@ export const mapCertificate = ( cert_index: certModel.cert_index, dRep: mapDrepDelegation(certModel), deposit: BigInt(certModel.deposit), - stakeCredential: { - hash: Cardano.RewardAccount.toHash( - Cardano.RewardAccount(certModel.address) - ) as unknown as Crypto.Hash28ByteBase16, - type: Cardano.CredentialType.KeyHash - } + stakeCredential: Cardano.Address.fromBech32(certModel.address).asReward()!.getPaymentCredential() }; if (isStakeVoteDelegationCertModel(certModel)) @@ -320,12 +312,7 @@ export const mapCertificate = ( cert_index: certModel.cert_index, dRep: mapDrepDelegation(certModel), poolId: certModel.pool_id as unknown as Cardano.PoolId, - stakeCredential: { - hash: Cardano.RewardAccount.toHash( - Cardano.RewardAccount(certModel.address) - ) as unknown as Crypto.Hash28ByteBase16, - type: Cardano.CredentialType.KeyHash - } + stakeCredential: Cardano.Address.fromBech32(certModel.address).asReward()!.getPaymentCredential() }; if (isStakeRegistrationDelegationCertModel(certModel)) @@ -334,12 +321,7 @@ export const mapCertificate = ( cert_index: certModel.cert_index, deposit: BigInt(certModel.deposit), poolId: certModel.pool_id as unknown as Cardano.PoolId, - stakeCredential: { - hash: Cardano.RewardAccount.toHash( - Cardano.RewardAccount(certModel.address) - ) as unknown as Crypto.Hash28ByteBase16, - type: Cardano.CredentialType.KeyHash - } + stakeCredential: Cardano.Address.fromBech32(certModel.address).asReward()!.getPaymentCredential() }; if (isStakeVoteRegistrationDelegationCertModel(certModel)) @@ -349,12 +331,7 @@ export const mapCertificate = ( dRep: mapDrepDelegation(certModel), deposit: BigInt(certModel.deposit), poolId: certModel.pool_id as unknown as Cardano.PoolId, - stakeCredential: { - hash: Cardano.RewardAccount.toHash( - Cardano.RewardAccount(certModel.address) - ) as unknown as Crypto.Hash28ByteBase16, - type: Cardano.CredentialType.KeyHash - } + stakeCredential: Cardano.Address.fromBech32(certModel.address).asReward()!.getPaymentCredential() }; if (isAuthorizeCommitteeHotCertModel(certModel)) diff --git a/packages/cardano-services/src/ChainHistory/DbSyncChainHistory/queries.ts b/packages/cardano-services/src/ChainHistory/DbSyncChainHistory/queries.ts index 129b0210d75..0fb98dadfc7 100644 --- a/packages/cardano-services/src/ChainHistory/DbSyncChainHistory/queries.ts +++ b/packages/cardano-services/src/ChainHistory/DbSyncChainHistory/queries.ts @@ -250,15 +250,21 @@ export const findPoolRetireCertsTxIds = ` export const findPoolRegisterCertsByTxIds = ` SELECT cert.cert_index AS cert_index, - pool."view" AS pool_id, + pool.view AS pool_id, tx.hash AS tx_id, CASE WHEN cert.deposit IS NULL THEN '0' ELSE cert.deposit - END AS deposit + END AS deposit, + stake_address.view AS reward_account, + pledge, + fixed_cost, + margin, + vrf_key_hash FROM tx JOIN pool_update AS cert ON cert.registered_tx_id = tx.id JOIN pool_hash AS pool ON pool.id = cert.hash_id + JOIN stake_address ON stake_address.id = reward_addr_id WHERE tx.id = ANY($1) ORDER BY tx.id ASC`; diff --git a/packages/cardano-services/src/ChainHistory/DbSyncChainHistory/types.ts b/packages/cardano-services/src/ChainHistory/DbSyncChainHistory/types.ts index 1790a03f792..c95253af48e 100644 --- a/packages/cardano-services/src/ChainHistory/DbSyncChainHistory/types.ts +++ b/packages/cardano-services/src/ChainHistory/DbSyncChainHistory/types.ts @@ -178,6 +178,11 @@ export interface PoolRetireCertModel extends CertificateModel { export interface PoolRegisterCertModel extends CertificateModel { pool_id: string; deposit: string; + reward_account: string; + pledge: string; + fixed_cost: string; + margin: number; + vrf_key_hash: Buffer; } export interface MirCertModel extends CertificateModel { diff --git a/packages/cardano-services/test/ChainHistory/DbSyncChainHistoryProvider/mappers.test.ts b/packages/cardano-services/test/ChainHistory/DbSyncChainHistoryProvider/mappers.test.ts index ac57bfb550e..9e529736eb7 100644 --- a/packages/cardano-services/test/ChainHistory/DbSyncChainHistoryProvider/mappers.test.ts +++ b/packages/cardano-services/test/ChainHistory/DbSyncChainHistoryProvider/mappers.test.ts @@ -34,6 +34,7 @@ const blockHash = '7a48b034645f51743550bbaf81f8a14771e58856e031eb63844738ca8ad72 const poolId = 'pool1zuevzm3xlrhmwjw87ec38mzs02tlkwec9wxpgafcaykmwg7efhh'; const datetime = '2022-05-10T19:22:43.620Z'; const vrfKey = 'vrf_vk19j362pkr4t9y0m3qxgmrv0365vd7c4ze03ny4jh84q8agjy4ep4s99zvg8'; +const vrfKeyHash = '220ba9398e3e5fae23a83d0d5927649d577a5f69d6ef1d5253c259d9393ba294'; const genesisLeaderHash = 'eff1b5b26e65b791d6f236c7c0264012bd1696759d22bdb4dd0f6f56'; const transactionHash = 'cefd2fcf657e5e5d6c35975f4e052f427819391b153ebb16ad8aa107ba5a3819'; const sourceTransactionHash = 'cefd2fcf657e5e5d6c35975f4e052f427819391b153ebb16ad8aa107ba5a3812'; @@ -292,14 +293,28 @@ describe('chain history mappers', () => { const result = mappers.mapCertificate({ ...baseCertModel, deposit: '500000000', + fixed_cost: '390000000', + margin: 0.15, + pledge: '420000000', pool_id: poolId, - type: 'register' + reward_account: stakeAddress, + type: 'register', + vrf_key_hash: Buffer.from(vrfKeyHash, 'hex') } as WithCertType); expect(result).toEqual>({ __typename: Cardano.CertificateType.PoolRegistration, cert_index: 0, deposit: 500_000_000n, - poolParameters: null as unknown as Cardano.PoolParameters + poolParameters: { + cost: 390_000_000n, + id: poolId as Cardano.PoolId, + margin: { denominator: 20, numerator: 3 }, + owners: [], + pledge: 420_000_000n, + relays: [], + rewardAccount: stakeAddress as Cardano.RewardAccount, + vrf: vrfKeyHash as Cardano.VrfVkHex + } }); }); test('map MirCertModel to Cardano.MirCertificate', () => { diff --git a/packages/e2e/test/wallet_epoch_3/SharedWallet/delegation.test.ts b/packages/e2e/test/wallet_epoch_3/SharedWallet/delegation.test.ts index 016e597b125..58b7009fe7e 100644 --- a/packages/e2e/test/wallet_epoch_3/SharedWallet/delegation.test.ts +++ b/packages/e2e/test/wallet_epoch_3/SharedWallet/delegation.test.ts @@ -1,6 +1,6 @@ /* eslint-disable max-statements */ import { BaseWallet, ObservableWallet } from '@cardano-sdk/wallet'; -import { BigIntMath, isNotNil } from '@cardano-sdk/util'; +import { BigIntMath, isNotNil, toSerializableObject } from '@cardano-sdk/util'; import { Cardano, Serialization, StakePoolProvider } from '@cardano-sdk/core'; import { TX_TIMEOUT_DEFAULT, @@ -204,6 +204,16 @@ describe('SharedWallet/delegation', () => { await waitForTx(aliceMultiSigWallet, tx.id); const tx1ConfirmedState = await getWalletStateSnapshot(aliceMultiSigWallet); + // Check Registration and StakeDelegation certificate from ChainHistoryProvider + let gotTx = toSerializableObject( + (await firstValueFrom(aliceMultiSigWallet.transactions.history$)).find((t) => tx.id === t.id)! + ); + // These are required because txBuilder still uses StakeRegistration + (gotTx as Cardano.HydratedTx).body.certificates![0].__typename = Cardano.CertificateType.StakeRegistration; + delete ((gotTx as Cardano.HydratedTx).body.certificates![0] as Partial).deposit; + expect((gotTx as Cardano.HydratedTx).body.certificates?.length).toEqual(2); + expect((gotTx as Cardano.HydratedTx).body.certificates).toEqual(toSerializableObject(tx.body.certificates)); + // Updates total and available balance after tx is on-chain expect(tx1ConfirmedState.balance.total.coins).toBe(expectedCoinsAfterTx1); expect(tx1ConfirmedState.balance.total).toEqual(tx1ConfirmedState.balance.available); @@ -237,6 +247,16 @@ describe('SharedWallet/delegation', () => { await waitForTx(aliceMultiSigWallet, tx.id); const tx2ConfirmedState = await getWalletStateSnapshot(aliceMultiSigWallet); + // Check Unregistration certificate from ChainHistoryProvider + gotTx = toSerializableObject( + (await firstValueFrom(aliceMultiSigWallet.transactions.history$)).find((t) => tx.id === t.id)! + ); + // These are required because txBuilder still uses StakeDeregistration + (gotTx as Cardano.HydratedTx).body.certificates![0].__typename = Cardano.CertificateType.StakeDeregistration; + delete ((gotTx as Cardano.HydratedTx).body.certificates![0] as Partial).deposit; + expect((gotTx as Cardano.HydratedTx).body.certificates?.length).toEqual(1); + expect((gotTx as Cardano.HydratedTx).body.certificates).toEqual(toSerializableObject(tx.body.certificates)); + // No longer delegating expect(tx2ConfirmedState.rewardAccount.delegatee?.nextNextEpoch?.id).toBeUndefined(); expect(tx2ConfirmedState.rewardAccount.credentialStatus).toBe(Cardano.StakeCredentialStatus.Unregistered);