Skip to content

Commit e7f9704

Browse files
Merge pull request #98 from IntersectMBO/feat/add-utxo-core
feat: create core utxo
2 parents 0731d43 + c556f18 commit e7f9704

File tree

462 files changed

+8804
-4435
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

462 files changed

+8804
-4435
lines changed

.changeset/cruel-rice-sort.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
---
2+
"@evolution-sdk/devnet": patch
3+
"@evolution-sdk/evolution": patch
4+
---
5+
6+
Migrate transaction builder and provider layer to use Core UTxO types throughout the SDK.
7+
8+
### New Core Types
9+
10+
- **`Core.UTxO`** — Schema-validated UTxO with branded types (`TransactionHash`, `Address`, `Assets`)
11+
- **`Core.Assets`** — Enhanced with `merge`, `subtract`, `negate`, `getAsset`, `setAsset`, `hasAsset` operations
12+
- **`Core.Time`** — New module for slot/time conversions with `SlotConfig`, `Slot`, `UnixTime`
13+
- **`Core.Address`** — Added `getAddressDetails`, `getPaymentCredential`, `getStakingCredential` utilities
14+
15+
### SDK Changes
16+
17+
- Provider methods (`getUtxos`, `getUtxoByUnit`, `getUtxosWithUnit`) now return `Core.UTxO.UTxO[]`
18+
- Client methods (`getWalletUtxos`, `newTx`) use Core UTxO internally
19+
- Transaction builder accepts `Core.UTxO.UTxO[]` for `availableUtxos`
20+
- `Genesis.calculateUtxosFromConfig` and `Genesis.queryUtxos` return Core UTxOs
21+
22+
### Rationale
23+
24+
The SDK previously used a lightweight `{ txHash, outputIndex, address, assets }` record for UTxOs, requiring constant conversions when interfacing with the Core layer (transaction building, CBOR serialization). This caused:
25+
26+
1. **Conversion overhead** — Every transaction build required converting SDK UTxOs to Core types
27+
2. **Type ambiguity**`txHash: string` vs `TransactionHash`, `address: string` vs `Address` led to runtime errors
28+
3. **Inconsistent APIs** — Some methods returned Core types, others SDK types
29+
30+
By standardizing on Core UTxO:
31+
32+
- **Zero conversion** — UTxOs flow directly from provider → wallet → builder → transaction
33+
- **Type safety** — Branded types prevent mixing up transaction hashes, addresses, policy IDs
34+
- **Unified model** — Single UTxO representation across the entire SDK
35+
36+
### Migration
37+
38+
```typescript
39+
// Before
40+
const lovelace = Assets.getAsset(utxo.assets, "lovelace")
41+
const txId = utxo.txHash
42+
const idx = utxo.outputIndex
43+
const addr = utxo.address // string
44+
45+
// After
46+
const lovelace = utxo.assets.lovelace
47+
const txId = Core.TransactionHash.toHex(utxo.transactionId)
48+
const idx = utxo.index // bigint
49+
const addr = Core.Address.toBech32(utxo.address) // or use Address directly
50+
```

docs/content/docs/clients/architecture.mdx

Lines changed: 29 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -34,25 +34,27 @@ interface ReadOnlyWalletConfig {
3434
```typescript twoslash
3535
import { createClient, Core } from "@evolution-sdk/evolution";
3636

37-
// Backend: read-only wallet with user's address (from frontend)
38-
const backendClient = createClient({
37+
// Backend: Create provider client, then attach read-only wallet
38+
const providerClient = createClient({
3939
network: "mainnet",
4040
provider: {
4141
type: "blockfrost",
4242
baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0",
4343
projectId: process.env.BLOCKFROST_PROJECT_ID!
44-
},
45-
wallet: {
46-
type: "read-only",
47-
address: "addr1qz8eg0aknl96hd3v6x3qfmmz5zhtrq5hn8hmq0x4qd6m2qdppx88rnw3eumv9zv2ctjns05c8jhsqwg98qaxcz2qh45qhjv39c"
4844
}
4945
});
5046

47+
// Attach user's address as read-only wallet (expects bech32 string)
48+
const backendClient = providerClient.attachWallet({
49+
type: "read-only",
50+
address: "addr1qz8eg0aknl96hd3v6x3qfmmz5zhtrq5hn8hmq0x4qd6m2qdppx88rnw3eumv9zv2ctjns05c8jhsqwg98qaxcz2qh45qhjv39c"
51+
});
52+
5153
// Build unsigned transaction
5254
const builder = backendClient.newTx();
5355
builder.payToAddress({
54-
address: "addr1...",
55-
assets: { lovelace: 5000000n }
56+
address: Core.Address.fromBech32("addr1qz8eg0aknl96hd3v6x3qfmmz5zhtrq5hn8hmq0x4qd6m2qdppx88rnw3eumv9zv2ctjns05c8jhsqwg98qaxcz2qh45qhjv39c"),
57+
assets: Core.Assets.fromLovelace(5_000_000n)
5658
});
5759

5860
// Build returns result, get transaction and serialize
@@ -157,8 +159,8 @@ export async function buildTransaction(userAddress: string) {
157159
// Build unsigned transaction
158160
const builder = client.newTx();
159161
builder.payToAddress({
160-
address: "addr1...",
161-
assets: { lovelace: 5000000n }
162+
address: Core.Address.fromBech32("addr1qz8eg0aknl96hd3v6x3qfmmz5zhtrq5hn8hmq0x4qd6m2qdppx88rnw3eumv9zv2ctjns05c8jhsqwg98qaxcz2qh45qhjv39c"),
163+
assets: Core.Assets.fromLovelace(5_000_000n)
162164
});
163165

164166
// Returns unsigned transaction
@@ -191,7 +193,7 @@ export type BuildPaymentResponse = { txCbor: string };
191193

192194
// @filename: frontend.ts
193195
// ===== Frontend (Browser) =====
194-
import { createClient } from "@evolution-sdk/evolution";
196+
import { Core, createClient } from "@evolution-sdk/evolution";
195197
import type { BuildPaymentResponse } from "./shared";
196198

197199
declare const cardano: any;
@@ -204,8 +206,8 @@ async function sendPayment(recipientAddress: string, lovelace: bigint) {
204206
wallet: { type: "api", api: walletApi }
205207
});
206208

207-
// 2. Get user address
208-
const userAddress = await walletClient.address();
209+
// 2. Get user address (returns Core Address, convert to bech32 for backend)
210+
const userAddress = Core.Address.toBech32(await walletClient.address());
209211

210212
// 3. Request backend to build transaction
211213
const response = await fetch("/api/build-payment", {
@@ -233,29 +235,34 @@ async function sendPayment(recipientAddress: string, lovelace: bigint) {
233235
import { createClient, Core } from "@evolution-sdk/evolution";
234236

235237
export async function buildPayment(
236-
userAddress: string,
237-
recipientAddress: string,
238+
userAddressBech32: string,
239+
recipientAddressBech32: string,
238240
lovelace: bigint
239241
) {
240-
// Create read-only client with user's address
241-
const client = createClient({
242+
// Convert bech32 addresses from frontend to Core Address
243+
const recipientAddress = Core.Address.fromBech32(recipientAddressBech32);
244+
245+
// Create provider client first, then attach read-only wallet
246+
const providerClient = createClient({
242247
network: "mainnet",
243248
provider: {
244249
type: "blockfrost",
245250
baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0",
246251
projectId: process.env.BLOCKFROST_PROJECT_ID!
247-
},
248-
wallet: {
249-
type: "read-only",
250-
address: userAddress
251252
}
252253
});
253254

255+
// Attach user's address as read-only wallet
256+
const client = providerClient.attachWallet({
257+
type: "read-only",
258+
address: userAddressBech32
259+
});
260+
254261
// Build unsigned transaction
255262
const builder = client.newTx();
256263
builder.payToAddress({
257264
address: recipientAddress,
258-
assets: { lovelace }
265+
assets: Core.Assets.fromLovelace(lovelace)
259266
});
260267

261268
// Return unsigned CBOR for frontend to sign

docs/content/docs/clients/architecture/frontend-backend.mdx

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,8 @@ Backend services use read-only clients configured with user addresses to build u
8383
```typescript twoslash
8484
import { createClient, Core } from "@evolution-sdk/evolution";
8585

86-
export async function buildTransaction(userAddress: string) {
87-
// Create read-only client with user's address
86+
export async function buildTransaction(userAddressBech32: string) {
87+
// Create read-only client with user's address (bech32 string)
8888
const client = createClient({
8989
network: "mainnet",
9090
provider: {
@@ -94,15 +94,15 @@ export async function buildTransaction(userAddress: string) {
9494
},
9595
wallet: {
9696
type: "read-only",
97-
address: userAddress
97+
address: userAddressBech32
9898
}
9999
});
100100

101101
// Build unsigned transaction
102102
const builder = client.newTx();
103103
builder.payToAddress({
104-
address: "addr1qz8eg0aknl96hd3v6x3qfmmz5zhtrq5hn8hmq0x4qd6m2qdppx88rnw3eumv9zv2ctjns05c8jhsqwg98qaxcz2qh45qhjv39c",
105-
assets: { lovelace: 5000000n }
104+
address: Core.Address.fromBech32("addr1qz8eg0aknl96hd3v6x3qfmmz5zhtrq5hn8hmq0x4qd6m2qdppx88rnw3eumv9zv2ctjns05c8jhsqwg98qaxcz2qh45qhjv39c"),
105+
assets: Core.Assets.fromLovelace(5000000n)
106106
});
107107

108108
// Build and return unsigned transaction
@@ -142,7 +142,7 @@ export type BuildPaymentResponse = {
142142

143143
// @filename: frontend.ts
144144
// ===== Frontend (Browser) =====
145-
import { createClient } from "@evolution-sdk/evolution";
145+
import { Core, createClient } from "@evolution-sdk/evolution";
146146
import type { BuildPaymentRequest, BuildPaymentResponse } from "./shared";
147147

148148
declare const cardano: any;
@@ -157,8 +157,8 @@ async function sendPayment(recipientAddress: string, lovelace: bigint) {
157157
wallet: { type: "api", api: walletApi }
158158
});
159159

160-
// 3. Get user address
161-
const userAddress = await walletClient.address();
160+
// 3. Get user address (returns Core Address, convert to bech32 for backend)
161+
const userAddress = Core.Address.toBech32(await walletClient.address());
162162

163163
// 4. Request backend to build transaction
164164
const requestBody: BuildPaymentRequest = {
@@ -190,11 +190,14 @@ import { createClient, Core } from "@evolution-sdk/evolution";
190190
import type { BuildPaymentResponse } from "./shared";
191191

192192
export async function buildPayment(
193-
userAddress: string,
194-
recipientAddress: string,
193+
userAddressBech32: string,
194+
recipientAddressBech32: string,
195195
lovelace: bigint
196196
): Promise<BuildPaymentResponse> {
197-
// Create read-only client with user's address
197+
// Convert recipient to Core Address for payToAddress
198+
const recipientAddress = Core.Address.fromBech32(recipientAddressBech32);
199+
200+
// Create read-only client with user's address (bech32 string)
198201
const client = createClient({
199202
network: "mainnet",
200203
provider: {
@@ -204,15 +207,15 @@ export async function buildPayment(
204207
},
205208
wallet: {
206209
type: "read-only",
207-
address: userAddress
210+
address: userAddressBech32
208211
}
209212
});
210213

211214
// Build unsigned transaction
212215
const builder = client.newTx();
213216
builder.payToAddress({
214217
address: recipientAddress,
215-
assets: { lovelace }
218+
assets: Core.Assets.fromLovelace(lovelace)
216219
});
217220

218221
// Return unsigned CBOR for frontend to sign

docs/content/docs/clients/client-basics.mdx

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ Evolution SDK follows a three-stage pattern: build, sign, submit. Each stage ret
4040
Start with `client.newTx()` and chain operations to specify outputs, metadata, or validity ranges:
4141

4242
```ts twoslash
43-
import { createClient } from "@evolution-sdk/evolution";
43+
import { createClient, Core } from "@evolution-sdk/evolution";
4444

4545
const client = createClient({
4646
network: "preprod",
@@ -51,8 +51,8 @@ const client = createClient({
5151
const builder = client.newTx();
5252

5353
builder.payToAddress({
54-
address: "addr_test1qzx9hu8j4ah3auytk0mwcupd69hpc52t0cw39a65ndrah86djs784u92a3m5w475w3w35tyd6v3qumkze80j8a6h5tuqq5xe8y",
55-
assets: { lovelace: 2000000n }
54+
address: Core.Address.fromBech32("addr_test1qzx9hu8j4ah3auytk0mwcupd69hpc52t0cw39a65ndrah86djs784u92a3m5w475w3w35tyd6v3qumkze80j8a6h5tuqq5xe8y"),
55+
assets: Core.Assets.fromLovelace(2000000n)
5656
});
5757

5858
const signBuilder = await builder.build();
@@ -63,7 +63,7 @@ const signBuilder = await builder.build();
6363
Call `.sign()` on the built transaction to create signatures with your wallet:
6464

6565
```ts twoslash
66-
import { createClient } from "@evolution-sdk/evolution";
66+
import { createClient, Core } from "@evolution-sdk/evolution";
6767

6868
const client = createClient({
6969
network: "preprod",
@@ -72,7 +72,7 @@ const client = createClient({
7272
});
7373

7474
const builder = client.newTx();
75-
builder.payToAddress({ address: "", assets: { lovelace: 2000000n } });
75+
builder.payToAddress({ address: Core.Address.fromBech32("addr_test1qzx9hu8j4ah3auytk0mwcupd69hpc52t0cw39a65ndrah86djs784u92a3m5w475w3w35tyd6v3qumkze80j8a6h5tuqq5xe8y"), assets: Core.Assets.fromLovelace(2000000n) });
7676
const signBuilder = await builder.build();
7777

7878
const submitBuilder = await signBuilder.sign();
@@ -83,7 +83,7 @@ const submitBuilder = await signBuilder.sign();
8383
Finally, `.submit()` broadcasts the signed transaction to the blockchain and returns the transaction hash:
8484

8585
```ts twoslash
86-
import { createClient } from "@evolution-sdk/evolution";
86+
import { createClient, Core } from "@evolution-sdk/evolution";
8787

8888
const client = createClient({
8989
network: "preprod",
@@ -92,7 +92,7 @@ const client = createClient({
9292
});
9393

9494
const builder = client.newTx();
95-
builder.payToAddress({ address: "", assets: { lovelace: 2000000n } });
95+
builder.payToAddress({ address: Core.Address.fromBech32("addr_test1qzx9hu8j4ah3auytk0mwcupd69hpc52t0cw39a65ndrah86djs784u92a3m5w475w3w35tyd6v3qumkze80j8a6h5tuqq5xe8y"), assets: Core.Assets.fromLovelace(2000000n) });
9696
const signBuilder = await builder.build();
9797
const submitBuilder = await signBuilder.sign();
9898

@@ -105,7 +105,7 @@ console.log("Transaction submitted:", txId);
105105
Here's the complete workflow in a single example—from client creation through transaction submission:
106106

107107
```ts twoslash
108-
import { createClient } from "@evolution-sdk/evolution";
108+
import { createClient, Core } from "@evolution-sdk/evolution";
109109

110110
const client = createClient({
111111
network: "preprod",
@@ -124,8 +124,8 @@ const client = createClient({
124124
// Build transaction
125125
const builder = client.newTx();
126126
builder.payToAddress({
127-
address: "addr_test1qzx9hu8j4ah3auytk0mwcupd69hpc52t0cw39a65ndrah86djs784u92a3m5w475w3w35tyd6v3qumkze80j8a6h5tuqq5xe8y",
128-
assets: { lovelace: 2000000n }
127+
address: Core.Address.fromBech32("addr_test1qzx9hu8j4ah3auytk0mwcupd69hpc52t0cw39a65ndrah86djs784u92a3m5w475w3w35tyd6v3qumkze80j8a6h5tuqq5xe8y"),
128+
assets: Core.Assets.fromLovelace(2000000n)
129129
});
130130

131131
// Build, sign, and submit

docs/content/docs/devnet/configuration.mdx

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,11 @@ const client = createClient({
3535
}
3636
});
3737

38-
// Get the address in Bech32 format
39-
const addressBech32 = await client.address();
38+
// Get the address
39+
const address = await client.address();
4040

4141
// Convert to hex for genesis configuration
42-
const addressHex = Core.Address.toHex(Core.Address.fromBech32(addressBech32));
42+
const addressHex = Core.Address.toHex(address);
4343

4444
// Create custom genesis with funded address
4545
const genesisConfig = {
@@ -91,8 +91,8 @@ const client2 = createClient({
9191
}
9292
});
9393

94-
const address1 = Core.Address.toHex(Core.Address.fromBech32(await client1.address()));
95-
const address2 = Core.Address.toHex(Core.Address.fromBech32(await client2.address()));
94+
const address1 = Core.Address.toHex(await client1.address());
95+
const address2 = Core.Address.toHex(await client2.address());
9696

9797
const genesisConfig = {
9898
...Config.DEFAULT_SHELLEY_GENESIS,
@@ -121,6 +121,7 @@ After creating a genesis configuration, calculate the resulting UTxOs to verify
121121

122122
```typescript twoslash
123123
import { Config, Genesis } from "@evolution-sdk/devnet";
124+
import { Core } from "@evolution-sdk/evolution";
124125
declare const addressHex: string;
125126

126127
const genesisConfig = {
@@ -135,10 +136,10 @@ const genesisUtxos = await Genesis.calculateUtxosFromConfig(genesisConfig);
135136

136137
console.log("Genesis UTxOs:", genesisUtxos.length);
137138
genesisUtxos.forEach(utxo => {
138-
console.log("Address:", utxo.address);
139+
console.log("Address:", Core.Address.toBech32(utxo.address));
139140
console.log("Amount:", utxo.assets.lovelace, "lovelace");
140-
console.log("TxHash:", utxo.txHash);
141-
console.log("OutputIndex:", utxo.outputIndex);
141+
console.log("TxHash:", Core.TransactionHash.toHex(utxo.transactionId));
142+
console.log("OutputIndex:", utxo.index);
142143
});
143144
```
144145

@@ -324,8 +325,8 @@ const wallet2 = createClient({
324325
}
325326
});
326327

327-
const addr1 = Core.Address.toHex(Core.Address.fromBech32(await wallet1.address()));
328-
const addr2 = Core.Address.toHex(Core.Address.fromBech32(await wallet2.address()));
328+
const addr1 = Core.Address.toHex(await wallet1.address());
329+
const addr2 = Core.Address.toHex(await wallet2.address());
329330

330331
// Custom genesis configuration
331332
const genesisConfig = {
@@ -453,7 +454,7 @@ With custom genesis configuration, you can now:
453454

454455
## Troubleshooting
455456

456-
**Address format errors**: Ensure addresses are in hexadecimal format, not Bech32. Use `Core.Address.toHex(Core.Address.fromBech32(addr))` to convert.
457+
**Address format errors**: Ensure addresses are in hexadecimal format, not Bech32. Use `Core.Address.toHex(address)` to convert from an Address object.
457458

458459
**Genesis UTxO not found**: Wait 3-5 seconds after cluster start for full initialization. Query timing matters for fast block configurations.
459460

0 commit comments

Comments
 (0)