Skip to content

Commit f4f6ac3

Browse files
Merge pull request #112 from IntersectMBO/feat/add-tx-chaining
feat/add tx chaining
2 parents 3c1210b + 6656a01 commit f4f6ac3

File tree

12 files changed

+347
-56
lines changed

12 files changed

+347
-56
lines changed

.changeset/cold-clubs-doubt.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
"@evolution-sdk/evolution": patch
3+
---
4+
5+
Add transaction chaining support via `SignBuilder.chainResult()`
6+
7+
- Add `chainResult()` method to `SignBuilder` for building dependent transactions
8+
- Returns `ChainResult` with `consumed`, `available` UTxOs and pre-computed `txHash`
9+
- Lazy evaluation with memoization - computed on first call, cached for subsequent calls
10+
- Add `signAndSubmit()` convenience method combining sign and submit in one call
11+
- Remove redundant `chain()`, `chainEffect()`, `chainEither()` methods from TransactionBuilder

docs/content/docs/modules/sdk/builders/SignBuilder.mdx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,21 @@ SignBuilder extends TransactionResultBase with signing capabilities.
2525
Only available when the client has a signing wallet (seed, private key, or API wallet).
2626
Provides access to unsigned transaction (via base interface) and signing operations.
2727

28+
Includes `chainResult` for transaction chaining - use `chainResult.available` as
29+
`availableUtxos` for the next transaction in a chain.
30+
2831
**Signature**
2932

3033
```ts
3134
export interface SignBuilder extends TransactionResultBase, EffectToPromiseAPI<SignBuilderEffect> {
3235
readonly Effect: SignBuilderEffect
36+
/**
37+
* Compute chain result for building dependent transactions.
38+
* Contains consumed UTxOs, available UTxOs (remaining + created), and txHash.
39+
*
40+
* Result is memoized - computed once on first call, cached for subsequent calls.
41+
*/
42+
readonly chainResult: () => ChainResult
3343
}
3444
```
3545

@@ -52,6 +62,7 @@ export interface SignBuilderEffect {
5262

5363
// Signing methods
5464
readonly sign: () => Effect.Effect<SubmitBuilder, TransactionBuilderError>
65+
readonly signAndSubmit: () => Effect.Effect<string, TransactionBuilderError>
5566
readonly signWithWitness: (
5667
witnessSet: TransactionWitnessSet.TransactionWitnessSet
5768
) => Effect.Effect<SubmitBuilder, TransactionBuilderError>

docs/content/docs/modules/sdk/builders/SignBuilderImpl.mdx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ export declare const makeSignBuilder: (params: {
4646
referenceUtxos: ReadonlyArray<CoreUTxO.UTxO>
4747
provider: Provider.Provider
4848
wallet: Wallet
49+
outputs: ReadonlyArray<TxOut.TransactionOutput>
50+
availableUtxos: ReadonlyArray<CoreUTxO.UTxO>
4951
}) => SignBuilder
5052
```
5153

docs/content/docs/modules/sdk/builders/TransactionBuilder.mdx

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -629,6 +629,36 @@ export interface TransactionBuilderBase {
629629
* @category builder-methods
630630
*/
631631
readonly addSigner: (params: AddSignerParams) => this
632+
633+
// ============================================================================
634+
// Transaction Chaining Methods
635+
// ============================================================================
636+
637+
/**
638+
* Execute transaction build and return consumed/available UTxOs for chaining.
639+
*
640+
* Runs the full build pipeline (coin selection, fee calculation, evaluation) and returns
641+
* which UTxOs were consumed and which remain available for subsequent transactions.
642+
* Use this when building multiple dependent transactions in sequence.
643+
*
644+
* @returns Promise<ChainResult> with consumed and available UTxOs
645+
*
646+
* @example
647+
* ```typescript
648+
* // Build first transaction, get remaining UTxOs
649+
* const tx1 = await builder
650+
* .payTo({ address, value: { lovelace: 5_000_000n } })
651+
* .build({ availableUtxos: walletUtxos })
652+
*
653+
* // Build second transaction using remaining UTxOs from chainResult
654+
* const tx2 = await builder
655+
* .payTo({ address, value: { lovelace: 3_000_000n } })
656+
* .build({ availableUtxos: tx1.chainResult().available })
657+
* ```
658+
*
659+
* @since 2.0.0
660+
* @category chaining-methods
661+
*/
632662
}
633663
````
634664
@@ -1021,17 +1051,22 @@ Added in v2.0.0
10211051

10221052
Result type for transaction chaining operations.
10231053

1024-
**NOTE: NOT YET IMPLEMENTED** - This interface is reserved for future implementation
1025-
of multi-transaction workflows. Current chain methods return stub implementations.
1054+
Provides consumed and available UTxOs for building chained transactions.
1055+
The available UTxOs include both remaining unspent inputs AND newly created outputs
1056+
with pre-computed txHash, ready to be spent in subsequent transactions.
1057+
1058+
Accessed via `SignBuilder.chainResult()` after calling `build()`.
10261059

10271060
**Signature**
10281061

10291062
```ts
10301063
export interface ChainResult {
1031-
readonly transaction: Transaction.Transaction
1032-
readonly newOutputs: ReadonlyArray<CoreUTxO.UTxO> // UTxOs created by this transaction
1033-
readonly updatedUtxos: ReadonlyArray<CoreUTxO.UTxO> // Available UTxOs for next transaction (original - spent + new)
1034-
readonly spentUtxos: ReadonlyArray<CoreUTxO.UTxO> // UTxOs consumed by this transaction
1064+
/** UTxOs consumed from availableUtxos by coin selection */
1065+
readonly consumed: ReadonlyArray<CoreUTxO.UTxO>
1066+
/** Available UTxOs: remaining unspent + newly created (with computed txHash) */
1067+
readonly available: ReadonlyArray<CoreUTxO.UTxO>
1068+
/** Pre-computed transaction hash (blake2b-256 of transaction body) */
1069+
readonly txHash: string
10351070
}
10361071
```
10371072

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import { afterAll, beforeAll, describe, expect, it } from "@effect/vitest"
2+
import * as Cluster from "@evolution-sdk/devnet/Cluster"
3+
import * as Config from "@evolution-sdk/devnet/Config"
4+
import * as Genesis from "@evolution-sdk/devnet/Genesis"
5+
import { Core } from "@evolution-sdk/evolution"
6+
import * as Address from "@evolution-sdk/evolution/core/Address"
7+
import type { SignBuilder } from "@evolution-sdk/evolution/sdk/builders/SignBuilder"
8+
import { createClient } from "@evolution-sdk/evolution/sdk/client/ClientImpl"
9+
10+
describe("TxBuilder.chainResult", () => {
11+
let devnetCluster: Cluster.Cluster | undefined
12+
let genesisConfig: Config.ShelleyGenesis
13+
let genesisUtxos: ReadonlyArray<Core.UTxO.UTxO> = []
14+
15+
const TEST_MNEMONIC =
16+
"test test test test test test test test test test test test test test test test test test test test test test test sauce"
17+
18+
const createTestClient = (accountIndex: number = 0) => {
19+
if (!devnetCluster) throw new Error("Cluster not initialized")
20+
const slotConfig = Cluster.getSlotConfig(devnetCluster)
21+
return createClient({
22+
network: 0,
23+
slotConfig,
24+
provider: {
25+
type: "kupmios",
26+
kupoUrl: "http://localhost:1449",
27+
ogmiosUrl: "http://localhost:1344"
28+
},
29+
wallet: {
30+
type: "seed",
31+
mnemonic: TEST_MNEMONIC,
32+
accountIndex,
33+
addressType: "Base"
34+
}
35+
})
36+
}
37+
38+
beforeAll(async () => {
39+
const tempClient = createClient({
40+
network: 0,
41+
wallet: { type: "seed", mnemonic: TEST_MNEMONIC, accountIndex: 0, addressType: "Base" }
42+
})
43+
44+
const testAddress = await tempClient.address()
45+
const testAddressHex = Address.toHex(testAddress)
46+
47+
genesisConfig = {
48+
...Config.DEFAULT_SHELLEY_GENESIS,
49+
slotLength: 0.02,
50+
epochLength: 50,
51+
activeSlotsCoeff: 1.0,
52+
initialFunds: { [testAddressHex]: 500_000_000_000 }
53+
}
54+
55+
genesisUtxos = await Genesis.calculateUtxosFromConfig(genesisConfig)
56+
57+
devnetCluster = await Cluster.make({
58+
clusterName: "chain-test",
59+
ports: { node: 6008, submit: 9009 },
60+
shelleyGenesis: genesisConfig,
61+
kupo: { enabled: true, port: 1449, logLevel: "Info" },
62+
ogmios: { enabled: true, port: 1344, logLevel: "info" }
63+
})
64+
65+
await Cluster.start(devnetCluster)
66+
await new Promise((resolve) => setTimeout(resolve, 5_000))
67+
}, 180_000)
68+
69+
afterAll(async () => {
70+
if (devnetCluster) {
71+
await Cluster.stop(devnetCluster)
72+
await Cluster.remove(devnetCluster)
73+
}
74+
}, 60_000)
75+
76+
it("should chain multiple transactions and submit them all", { timeout: 90_000 }, async () => {
77+
const client = createTestClient(0)
78+
const address = await client.address()
79+
const TX_COUNT = 5
80+
81+
// Build chained transactions using build() + chainResult
82+
let available = [...genesisUtxos]
83+
const txs: Array<SignBuilder> = []
84+
85+
for (let i = 0; i < TX_COUNT; i++) {
86+
const tx = await client
87+
.newTx()
88+
.payToAddress({ address, assets: Core.Assets.fromLovelace(10_000_000n) })
89+
.build({ availableUtxos: available })
90+
txs.push(tx)
91+
available = [...tx.chainResult().available]
92+
}
93+
94+
// Verify all txHashes are unique
95+
const txHashes = txs.map((tx) => tx.chainResult().txHash)
96+
expect(new Set(txHashes).size).toBe(TX_COUNT)
97+
98+
// Submit all transactions
99+
const submittedHashes: Array<string> = []
100+
for (const tx of txs) {
101+
const hash = await tx.signAndSubmit()
102+
submittedHashes.push(hash)
103+
}
104+
105+
// Verify computed hashes match submitted hashes
106+
for (let i = 0; i < TX_COUNT; i++) {
107+
expect(submittedHashes[i]).toBe(txs[i].chainResult().txHash)
108+
}
109+
110+
// Wait for all to confirm
111+
for (const hash of submittedHashes) {
112+
expect(await client.awaitTx(hash, 1000)).toBe(true)
113+
}
114+
})
115+
})

packages/evolution/docs/modules/sdk/builders/SignBuilder.ts.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,21 @@ SignBuilder extends TransactionResultBase with signing capabilities.
2525
Only available when the client has a signing wallet (seed, private key, or API wallet).
2626
Provides access to unsigned transaction (via base interface) and signing operations.
2727

28+
Includes `chainResult` for transaction chaining - use `chainResult.available` as
29+
`availableUtxos` for the next transaction in a chain.
30+
2831
**Signature**
2932

3033
```ts
3134
export interface SignBuilder extends TransactionResultBase, EffectToPromiseAPI<SignBuilderEffect> {
3235
readonly Effect: SignBuilderEffect
36+
/**
37+
* Compute chain result for building dependent transactions.
38+
* Contains consumed UTxOs, available UTxOs (remaining + created), and txHash.
39+
*
40+
* Result is memoized - computed once on first call, cached for subsequent calls.
41+
*/
42+
readonly chainResult: () => ChainResult
3343
}
3444
```
3545

@@ -52,6 +62,7 @@ export interface SignBuilderEffect {
5262

5363
// Signing methods
5464
readonly sign: () => Effect.Effect<SubmitBuilder, TransactionBuilderError>
65+
readonly signAndSubmit: () => Effect.Effect<string, TransactionBuilderError>
5566
readonly signWithWitness: (
5667
witnessSet: TransactionWitnessSet.TransactionWitnessSet
5768
) => Effect.Effect<SubmitBuilder, TransactionBuilderError>

packages/evolution/docs/modules/sdk/builders/SignBuilderImpl.ts.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ export declare const makeSignBuilder: (params: {
4646
referenceUtxos: ReadonlyArray<CoreUTxO.UTxO>
4747
provider: Provider.Provider
4848
wallet: Wallet
49+
outputs: ReadonlyArray<TxOut.TransactionOutput>
50+
availableUtxos: ReadonlyArray<CoreUTxO.UTxO>
4951
}) => SignBuilder
5052
```
5153

packages/evolution/docs/modules/sdk/builders/TransactionBuilder.ts.md

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -629,6 +629,36 @@ export interface TransactionBuilderBase {
629629
* @category builder-methods
630630
*/
631631
readonly addSigner: (params: AddSignerParams) => this
632+
633+
// ============================================================================
634+
// Transaction Chaining Methods
635+
// ============================================================================
636+
637+
/**
638+
* Execute transaction build and return consumed/available UTxOs for chaining.
639+
*
640+
* Runs the full build pipeline (coin selection, fee calculation, evaluation) and returns
641+
* which UTxOs were consumed and which remain available for subsequent transactions.
642+
* Use this when building multiple dependent transactions in sequence.
643+
*
644+
* @returns Promise<ChainResult> with consumed and available UTxOs
645+
*
646+
* @example
647+
* ```typescript
648+
* // Build first transaction, get remaining UTxOs
649+
* const tx1 = await builder
650+
* .payTo({ address, value: { lovelace: 5_000_000n } })
651+
* .build({ availableUtxos: walletUtxos })
652+
*
653+
* // Build second transaction using remaining UTxOs from chainResult
654+
* const tx2 = await builder
655+
* .payTo({ address, value: { lovelace: 3_000_000n } })
656+
* .build({ availableUtxos: tx1.chainResult().available })
657+
* ```
658+
*
659+
* @since 2.0.0
660+
* @category chaining-methods
661+
*/
632662
}
633663
````
634664
@@ -1021,17 +1051,22 @@ Added in v2.0.0
10211051

10221052
Result type for transaction chaining operations.
10231053

1024-
**NOTE: NOT YET IMPLEMENTED** - This interface is reserved for future implementation
1025-
of multi-transaction workflows. Current chain methods return stub implementations.
1054+
Provides consumed and available UTxOs for building chained transactions.
1055+
The available UTxOs include both remaining unspent inputs AND newly created outputs
1056+
with pre-computed txHash, ready to be spent in subsequent transactions.
1057+
1058+
Accessed via `SignBuilder.chainResult()` after calling `build()`.
10261059

10271060
**Signature**
10281061

10291062
```ts
10301063
export interface ChainResult {
1031-
readonly transaction: Transaction.Transaction
1032-
readonly newOutputs: ReadonlyArray<CoreUTxO.UTxO> // UTxOs created by this transaction
1033-
readonly updatedUtxos: ReadonlyArray<CoreUTxO.UTxO> // Available UTxOs for next transaction (original - spent + new)
1034-
readonly spentUtxos: ReadonlyArray<CoreUTxO.UTxO> // UTxOs consumed by this transaction
1064+
/** UTxOs consumed from availableUtxos by coin selection */
1065+
readonly consumed: ReadonlyArray<CoreUTxO.UTxO>
1066+
/** Available UTxOs: remaining unspent + newly created (with computed txHash) */
1067+
readonly available: ReadonlyArray<CoreUTxO.UTxO>
1068+
/** Pre-computed transaction hash (blake2b-256 of transaction body) */
1069+
readonly txHash: string
10351070
}
10361071
```
10371072

packages/evolution/src/sdk/builders/SignBuilder.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type * as Transaction from "../../core/Transaction.js"
44
import type * as TransactionWitnessSet from "../../core/TransactionWitnessSet.js"
55
import type { EffectToPromiseAPI } from "../Type.js"
66
import type { SubmitBuilder } from "./SubmitBuilder.js"
7-
import type { TransactionBuilderError } from "./TransactionBuilder.js"
7+
import type { ChainResult, TransactionBuilderError } from "./TransactionBuilder.js"
88
import type { TransactionResultBase } from "./TransactionResult.js"
99

1010
// ============================================================================
@@ -27,6 +27,7 @@ export interface SignBuilderEffect {
2727

2828
// Signing methods
2929
readonly sign: () => Effect.Effect<SubmitBuilder, TransactionBuilderError>
30+
readonly signAndSubmit: () => Effect.Effect<string, TransactionBuilderError>
3031
readonly signWithWitness: (
3132
witnessSet: TransactionWitnessSet.TransactionWitnessSet
3233
) => Effect.Effect<SubmitBuilder, TransactionBuilderError>
@@ -43,9 +44,19 @@ export interface SignBuilderEffect {
4344
* Only available when the client has a signing wallet (seed, private key, or API wallet).
4445
* Provides access to unsigned transaction (via base interface) and signing operations.
4546
*
47+
* Includes `chainResult` for transaction chaining - use `chainResult.available` as
48+
* `availableUtxos` for the next transaction in a chain.
49+
*
4650
* @since 2.0.0
4751
* @category interfaces
4852
*/
4953
export interface SignBuilder extends TransactionResultBase, EffectToPromiseAPI<SignBuilderEffect> {
5054
readonly Effect: SignBuilderEffect
55+
/**
56+
* Compute chain result for building dependent transactions.
57+
* Contains consumed UTxOs, available UTxOs (remaining + created), and txHash.
58+
*
59+
* Result is memoized - computed once on first call, cached for subsequent calls.
60+
*/
61+
readonly chainResult: () => ChainResult
5162
}

0 commit comments

Comments
 (0)