Skip to content

Commit 9ddc79d

Browse files
committed
feat: add required signers and native scripts working as utxo ref
1 parent a39399c commit 9ddc79d

File tree

17 files changed

+1212
-109
lines changed

17 files changed

+1212
-109
lines changed

.changeset/common-ways-occur.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
"@evolution-sdk/evolution": patch
3+
---
4+
5+
### Native Scripts & Multi-Sig Support
6+
7+
- **`addSigner` operation**: Add required signers to transactions for multi-sig and script validation
8+
- **Native script minting**: Full support for `ScriptAll`, `ScriptAny`, `ScriptNOfK`, `InvalidBefore`, `InvalidHereafter`
9+
- **Reference scripts**: Use native scripts via `readFrom` instead of attaching them to transactions
10+
- **Multi-sig spending**: Spend from native script addresses with multi-party signing
11+
- **Improved fee calculation**: Accurate fee estimation for transactions with native scripts and reference scripts
12+
13+
### API Changes
14+
15+
- `UTxO.scriptRef` type changed from `ScriptRef` to `Script` for better type safety
16+
- `PayToAddressParams.scriptRef` renamed to `script` for consistency
17+
- Wallet `signTx` now accepts `referenceUtxos` context for native script signer detection
18+
- Client `signTx` auto-fetches reference UTxOs when signing transactions with reference inputs
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
/**
2+
* Devnet tests for TxBuilder addSigner operation.
3+
*
4+
* Tests the addSigner operation which adds required signers (key hashes)
5+
* to the transaction body's requiredSigners field.
6+
*/
7+
8+
import { afterAll, beforeAll, describe, expect, it } from "@effect/vitest"
9+
import * as Cluster from "@evolution-sdk/devnet/Cluster"
10+
import * as Config from "@evolution-sdk/devnet/Config"
11+
import * as Genesis from "@evolution-sdk/devnet/Genesis"
12+
import { Core } from "@evolution-sdk/evolution"
13+
import * as Address from "@evolution-sdk/evolution/core/Address"
14+
import * as KeyHash from "@evolution-sdk/evolution/core/KeyHash"
15+
import { createClient } from "@evolution-sdk/evolution/sdk/client/ClientImpl"
16+
17+
describe("TxBuilder addSigner (Devnet Submit)", () => {
18+
let devnetCluster: Cluster.Cluster | undefined
19+
let genesisConfig: Config.ShelleyGenesis
20+
let genesisUtxos: ReadonlyArray<Core.UTxO.UTxO> = []
21+
22+
const TEST_MNEMONIC =
23+
"test test test test test test test test test test test test test test test test test test test test test test test sauce"
24+
25+
const createTestClient = (accountIndex: number = 0) => {
26+
if (!devnetCluster) throw new Error("Cluster not initialized")
27+
const slotConfig = Cluster.getSlotConfig(devnetCluster)
28+
return createClient({
29+
network: 0,
30+
slotConfig,
31+
provider: {
32+
type: "kupmios",
33+
kupoUrl: "http://localhost:1449",
34+
ogmiosUrl: "http://localhost:1344"
35+
},
36+
wallet: {
37+
type: "seed",
38+
mnemonic: TEST_MNEMONIC,
39+
accountIndex,
40+
addressType: "Base"
41+
}
42+
})
43+
}
44+
45+
beforeAll(async () => {
46+
const tempClient = createClient({
47+
network: 0,
48+
wallet: { type: "seed", mnemonic: TEST_MNEMONIC, accountIndex: 0, addressType: "Base" }
49+
})
50+
51+
const testAddress = await tempClient.address()
52+
const testAddressHex = Address.toHex(testAddress)
53+
54+
genesisConfig = {
55+
...Config.DEFAULT_SHELLEY_GENESIS,
56+
slotLength: 0.02,
57+
epochLength: 50,
58+
activeSlotsCoeff: 1.0,
59+
initialFunds: { [testAddressHex]: 500_000_000_000 }
60+
}
61+
62+
genesisUtxos = await Genesis.calculateUtxosFromConfig(genesisConfig)
63+
64+
devnetCluster = await Cluster.make({
65+
clusterName: "addsigner-test",
66+
ports: { node: 6007, submit: 9008 },
67+
shelleyGenesis: genesisConfig,
68+
kupo: { enabled: true, port: 1449, logLevel: "Info" },
69+
ogmios: { enabled: true, port: 1344, logLevel: "info" }
70+
})
71+
72+
await Cluster.start(devnetCluster)
73+
await new Promise((resolve) => setTimeout(resolve, 3_000))
74+
}, 180_000)
75+
76+
afterAll(async () => {
77+
if (devnetCluster) {
78+
await Cluster.stop(devnetCluster)
79+
await Cluster.remove(devnetCluster)
80+
}
81+
}, 60_000)
82+
83+
it("should include requiredSigners in transaction body and submit successfully", { timeout: 60_000 }, async () => {
84+
const client = createTestClient(0)
85+
const myAddress = await client.address()
86+
87+
// Extract payment key hash from address credential
88+
const paymentCredential = myAddress.paymentCredential
89+
if (paymentCredential._tag !== "KeyHash") {
90+
throw new Error("Expected KeyHash credential")
91+
}
92+
93+
const signBuilder = await client
94+
.newTx()
95+
.addSigner({ keyHash: paymentCredential })
96+
.payToAddress({
97+
address: myAddress,
98+
assets: Core.Assets.fromLovelace(5_000_000n)
99+
})
100+
.build({ availableUtxos: [...genesisUtxos] })
101+
102+
const tx = await signBuilder.toTransaction()
103+
104+
// Verify requiredSigners is set
105+
expect(tx.body.requiredSigners).toBeDefined()
106+
expect(tx.body.requiredSigners?.length).toBe(1)
107+
expect(tx.body.requiredSigners?.[0]._tag).toBe("KeyHash")
108+
expect(KeyHash.toHex(tx.body.requiredSigners![0])).toBe(KeyHash.toHex(paymentCredential))
109+
110+
// Submit and verify confirmation
111+
const submitBuilder = await signBuilder.sign()
112+
const txHash = await submitBuilder.submit()
113+
114+
expect(txHash.length).toBe(64)
115+
116+
const confirmed = await client.awaitTx(txHash, 1000)
117+
expect(confirmed).toBe(true)
118+
})
119+
120+
it("should support multi-sig with partial signing and assembly", { timeout: 60_000 }, async () => {
121+
// Create two clients with different account indices (different key pairs)
122+
const client1 = createTestClient(0)
123+
const client2 = createTestClient(1)
124+
125+
const address1 = await client1.address()
126+
const address2 = await client2.address()
127+
128+
// Extract payment key hashes from both addresses
129+
const credential1 = address1.paymentCredential
130+
const credential2 = address2.paymentCredential
131+
132+
if (credential1._tag !== "KeyHash" || credential2._tag !== "KeyHash") {
133+
throw new Error("Expected KeyHash credentials")
134+
}
135+
136+
// Fetch fresh UTxOs from the provider (after first test has run)
137+
const freshUtxos = await client1.getUtxos(address1)
138+
139+
// Build a transaction requiring BOTH signers
140+
// Client1 builds and pays to self, but we require both keys
141+
const signBuilder = await client1
142+
.newTx()
143+
.addSigner({ keyHash: credential1 })
144+
.addSigner({ keyHash: credential2 })
145+
.payToAddress({
146+
address: address1,
147+
assets: Core.Assets.fromLovelace(5_000_000n)
148+
})
149+
.build({ availableUtxos: [...freshUtxos] })
150+
151+
const tx = await signBuilder.toTransaction()
152+
153+
// Verify both requiredSigners are set
154+
expect(tx.body.requiredSigners).toBeDefined()
155+
expect(tx.body.requiredSigners?.length).toBe(2)
156+
157+
const requiredHashes = tx.body.requiredSigners!.map((k) => KeyHash.toHex(k))
158+
expect(requiredHashes).toContain(KeyHash.toHex(credential1))
159+
expect(requiredHashes).toContain(KeyHash.toHex(credential2))
160+
161+
// Client1 creates a partial signature
162+
const witness1 = await signBuilder.partialSign()
163+
expect(witness1.vkeyWitnesses?.length).toBe(1)
164+
165+
// Client2 signs the SAME transaction (not rebuilding it)
166+
// Use the client's signTx method directly with the transaction object
167+
const witness2 = await client2.signTx(tx)
168+
expect(witness2.vkeyWitnesses?.length).toBe(1)
169+
170+
// Assemble both witnesses into the final transaction
171+
const submitBuilder = await signBuilder.assemble([witness1, witness2])
172+
173+
// Submit and verify confirmation
174+
const txHash = await submitBuilder.submit()
175+
expect(txHash.length).toBe(64)
176+
177+
const confirmed = await client1.awaitTx(txHash, 1000)
178+
expect(confirmed).toBe(true)
179+
})
180+
})

0 commit comments

Comments
 (0)