Skip to content

Commit e8f4296

Browse files
committed
fix(tx-construction): builder now awaits for non-empty knownAddresses$ before building the tx
1 parent faaf9b0 commit e8f4296

File tree

5 files changed

+121
-3
lines changed

5 files changed

+121
-3
lines changed

packages/tx-construction/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
"devDependencies": {
4141
"@cardano-sdk/util-dev": "workspace:~",
4242
"@types/lodash": "^4.14.182",
43+
"delay": "^5.0.0",
4344
"eslint": "^7.32.0",
4445
"jest": "^28.1.3",
4546
"madge": "^5.0.1",

packages/tx-construction/src/tx-builder/TxBuilder.ts

+8-3
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,10 @@ import { Logger } from 'ts-log';
2020
import { OutputBuilderValidator, TxOutputBuilder } from './OutputBuilder';
2121
import { RewardAccountWithPoolId } from '../types';
2222
import { coldObservableProvider } from '@cardano-sdk/util-rxjs';
23-
import { contextLogger, deepEquals } from '@cardano-sdk/util';
23+
import { contextLogger, deepEquals, patchObject } from '@cardano-sdk/util';
2424
import { createOutputValidator } from '../output-validation';
25+
import { filter, firstValueFrom, lastValueFrom } from 'rxjs';
2526
import { finalizeTx } from './finalizeTx';
26-
import { firstValueFrom, lastValueFrom } from 'rxjs';
2727
import { initializeTx } from './initializeTx';
2828
import minBy from 'lodash/minBy';
2929

@@ -96,7 +96,12 @@ export class GenericTxBuilder implements TxBuilder {
9696
createOutputValidator({
9797
protocolParameters: dependencies.txBuilderProviders.protocolParameters
9898
});
99-
this.#dependencies = dependencies;
99+
this.#dependencies = {
100+
...dependencies,
101+
keyAgent: patchObject(dependencies.keyAgent, {
102+
knownAddresses$: dependencies.keyAgent.knownAddresses$.pipe(filter((addresses) => addresses.length > 0))
103+
})
104+
};
100105
this.#logger = dependencies.logger;
101106
this.#handleProvider = dependencies.handleProvider;
102107
this.#handles = [];
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/* eslint-disable sonarjs/no-duplicate-string */
2+
import * as Crypto from '@cardano-sdk/crypto';
3+
import { AddressType, AsyncKeyAgent, GroupedAddress, SignBlobResult, util } from '@cardano-sdk/key-management';
4+
import { BehaviorSubject, firstValueFrom } from 'rxjs';
5+
import { Cardano } from '@cardano-sdk/core';
6+
import { GenericTxBuilder, OutputValidation } from '../../src';
7+
import { dummyLogger } from 'ts-log';
8+
import { generateRandomHexString, mockProviders as mocks } from '@cardano-sdk/util-dev';
9+
import delay from 'delay';
10+
11+
class StubKeyAgent implements AsyncKeyAgent {
12+
knownAddresses$ = new BehaviorSubject<GroupedAddress[]>([]);
13+
14+
constructor(private inputResolver: Cardano.InputResolver) {}
15+
16+
deriveAddress(): Promise<GroupedAddress> {
17+
throw new Error('Method not implemented.');
18+
}
19+
derivePublicKey(): Promise<Crypto.Ed25519PublicKeyHex> {
20+
throw new Error('Method not implemented.');
21+
}
22+
signBlob(): Promise<SignBlobResult> {
23+
throw new Error('Method not implemented.');
24+
}
25+
async signTransaction(txInternals: Cardano.TxBodyWithHash): Promise<Cardano.Signatures> {
26+
const signatures = new Map<Crypto.Ed25519PublicKeyHex, Crypto.Ed25519SignatureHex>();
27+
const knownAddresses = await firstValueFrom(this.knownAddresses$);
28+
for (const _ of await util.ownSignatureKeyPaths(txInternals.body, knownAddresses, this.inputResolver)) {
29+
signatures.set(
30+
Crypto.Ed25519PublicKeyHex(generateRandomHexString(64)),
31+
Crypto.Ed25519SignatureHex(generateRandomHexString(128))
32+
);
33+
}
34+
return signatures;
35+
}
36+
getChainId(): Promise<Cardano.ChainId> {
37+
throw new Error('Method not implemented.');
38+
}
39+
getBip32Ed25519(): Promise<Crypto.Bip32Ed25519> {
40+
throw new Error('Method not implemented.');
41+
}
42+
getExtendedAccountPublicKey(): Promise<Crypto.Bip32PublicKeyHex> {
43+
throw new Error('Method not implemented.');
44+
}
45+
async setKnownAddresses(addresses: GroupedAddress[]): Promise<void> {
46+
this.knownAddresses$.next(addresses);
47+
}
48+
shutdown(): void {
49+
throw new Error('Method not implemented.');
50+
}
51+
}
52+
53+
describe('TxBuilder bootstrap', () => {
54+
it('awaits for non-empty knownAddresses$', async () => {
55+
// Initialize the TxBuilder
56+
const output = mocks.utxo[0][1];
57+
const rewardAccount = mocks.rewardAccount;
58+
const inputResolver: Cardano.InputResolver = {
59+
resolveInput: async (txIn) =>
60+
mocks.utxo.find(
61+
([hydratedTxIn]) => txIn.txId === hydratedTxIn.txId && txIn.index === hydratedTxIn.index
62+
)?.[1] || null
63+
};
64+
const keyAgent = new StubKeyAgent(inputResolver);
65+
const txBuilderProviders = {
66+
genesisParameters: jest.fn().mockResolvedValue(mocks.genesisParameters),
67+
protocolParameters: jest.fn().mockResolvedValue(mocks.protocolParameters),
68+
rewardAccounts: jest.fn().mockResolvedValue([
69+
{
70+
address: rewardAccount,
71+
keyStatus: Cardano.StakeKeyStatus.Unregistered,
72+
rewardBalance: mocks.rewardAccountBalance
73+
}
74+
]),
75+
tip: jest.fn().mockResolvedValue(mocks.ledgerTip),
76+
utxoAvailable: jest.fn().mockResolvedValue(mocks.utxo)
77+
};
78+
const outputValidator = {
79+
validateOutput: jest.fn().mockResolvedValue({ coinMissing: 0n } as OutputValidation)
80+
};
81+
const builderParams = {
82+
inputResolver,
83+
keyAgent,
84+
logger: dummyLogger,
85+
outputValidator,
86+
txBuilderProviders
87+
};
88+
const txBuilder = new GenericTxBuilder(builderParams);
89+
90+
// Build and sign a tx
91+
const signedTxReady = txBuilder.addOutput(output).build().sign();
92+
await delay(1);
93+
// keyAgent knownAddresses are initially [],
94+
// but then eventually resolves to some addresses
95+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
96+
keyAgent.setKnownAddresses([
97+
{
98+
accountIndex: 0,
99+
address: mocks.utxo[0][1].address,
100+
index: 0,
101+
networkId: Cardano.NetworkId.Testnet,
102+
rewardAccount: mocks.rewardAccount,
103+
type: AddressType.External
104+
}
105+
]);
106+
const signedTx = await signedTxReady;
107+
expect(signedTx.tx.witness.signatures.size).toBe(1);
108+
});
109+
});

packages/tx-construction/test/tx-builder/TxBuilderDelegatePortfolio.test.ts

+2
Original file line numberDiff line numberDiff line change
@@ -563,6 +563,7 @@ describe('TxBuilder/delegatePortfolio', () => {
563563
describe('rewardAccount syncing', () => {
564564
const normalRewardAccountsCalls = 3;
565565
it('can wait for delayed key agent stake keys', async () => {
566+
await keyAgent.deriveAddress({ index: 0, type: AddressType.External }, 0);
566567
const rewardAccountsProvider = jest
567568
.fn()
568569
.mockResolvedValueOnce([])
@@ -597,6 +598,7 @@ describe('TxBuilder/delegatePortfolio', () => {
597598
});
598599

599600
it('throws if new stake keys are not part of reward accounts in a reasonable time', async () => {
601+
await keyAgent.deriveAddress({ index: 0, type: AddressType.External }, 0);
600602
const rewardAccountsProvider = jest.fn().mockResolvedValue([]);
601603
const txBuilderFactory = await createTxBuilder({
602604
keyAgent,

yarn.lock

+1
Original file line numberDiff line numberDiff line change
@@ -2866,6 +2866,7 @@ __metadata:
28662866
"@cardano-sdk/util-rxjs": "workspace:~"
28672867
"@cardano-sdk/wallet": "workspace:~"
28682868
"@types/lodash": ^4.14.182
2869+
delay: ^5.0.0
28692870
eslint: ^7.32.0
28702871
jest: ^28.1.3
28712872
lodash: ^4.17.21

0 commit comments

Comments
 (0)