Skip to content

Commit fbb05a9

Browse files
committed
feat: upgra modules
1 parent a7462f6 commit fbb05a9

File tree

14 files changed

+599
-291
lines changed

14 files changed

+599
-291
lines changed

packages/evolution/src/sdk/Assets.ts

Lines changed: 146 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,84 @@
1+
import { Option } from "effect"
2+
3+
import * as AssetName from "../core/AssetName.js"
4+
import * as Coin from "../core/Coin.js"
5+
import * as MultiAsset from "../core/MultiAsset.js"
6+
import * as CorePolicyId from "../core/PolicyId.js"
7+
import * as PositiveCoin from "../core/PositiveCoin.js"
8+
import * as CoreValue from "../core/Value.js"
9+
import * as Unit from "./Unit.js"
10+
111
export interface Assets {
212
lovelace: bigint
313
[key: string]: bigint
414
}
515

16+
/**
17+
* Sort assets according to CBOR canonical ordering rules (RFC 7049 section 3.9).
18+
* Lovelace comes first, then assets sorted by policy ID length, then lexicographically.
19+
*/
20+
export const sortCanonical = (assets: Assets): Assets => {
21+
const entries = Object.entries(assets).sort(([aUnit], [bUnit]) => {
22+
const a = Unit.fromUnit(aUnit)
23+
const b = Unit.fromUnit(bUnit)
24+
25+
// Compare policy lengths
26+
// NOTE: all policies have the same length, because they must be 28 bytes
27+
// but because Lovelace is in the assets we must compare the length
28+
if (a.policyId.length !== b.policyId.length) {
29+
return a.policyId.length - b.policyId.length
30+
}
31+
32+
// If policy IDs are the same, compare asset names length
33+
if (a.policyId === b.policyId) {
34+
const aAssetName = a.assetName || ""
35+
const bAssetName = b.assetName || ""
36+
if (aAssetName.length !== bAssetName.length) {
37+
return aAssetName.length - bAssetName.length
38+
}
39+
// If asset names have same length, compare them lexicographically
40+
return aAssetName.localeCompare(bAssetName)
41+
}
42+
43+
// If policy IDs have same length but are different, compare them lexicographically
44+
return a.policyId.localeCompare(b.policyId)
45+
})
46+
47+
return Object.fromEntries(entries) as Assets
48+
}
49+
50+
/**
51+
* Multiply all asset amounts by a factor.
52+
* Useful for calculating fees, rewards, or scaling asset amounts.
53+
*/
54+
export const multiply = (assets: Assets, factor: bigint): Assets => {
55+
const result: Record<string, bigint> = { lovelace: assets.lovelace * factor }
56+
57+
for (const [unit, amount] of Object.entries(assets)) {
58+
if (unit !== "lovelace") {
59+
result[unit] = amount * factor
60+
}
61+
}
62+
63+
return result as Assets
64+
}
65+
66+
/**
67+
* Negate all asset amounts.
68+
* Useful for calculating what needs to be subtracted or for representing debts.
69+
*/
70+
export const negate = (assets: Assets): Assets => {
71+
const result: Record<string, bigint> = { lovelace: -assets.lovelace }
72+
73+
for (const [unit, amount] of Object.entries(assets)) {
74+
if (unit !== "lovelace") {
75+
result[unit] = -amount
76+
}
77+
}
78+
79+
return result as Assets
80+
}
81+
682
// Constructor and factory functions
783
export const make = (lovelace: bigint, tokens: Record<string, bigint> = {}): Assets => ({
884
lovelace,
@@ -91,19 +167,6 @@ export const hasAsset = (assets: Assets, unit: string): boolean => {
91167
return unit in assets && assets[unit] > 0n
92168
}
93169

94-
export const isValid = (assets: Assets): boolean => {
95-
try {
96-
// Check if it has required lovelace field and all values are bigint
97-
if (typeof assets.lovelace !== "bigint") return false
98-
for (const [unit, amount] of Object.entries(assets)) {
99-
if (unit !== "lovelace" && typeof amount !== "bigint") return false
100-
}
101-
return true
102-
} catch {
103-
return false
104-
}
105-
}
106-
107170
export const isEmpty = (assets: Assets): boolean => {
108171
if (assets.lovelace > 0n) return false
109172
for (const [unit, amount] of Object.entries(assets)) {
@@ -120,16 +183,78 @@ export const getUnits = (assets: Assets): Array<string> => {
120183
return units
121184
}
122185

123-
// String representation
124-
export const toString = (assets: Assets): string => {
125-
const tokens = []
126-
tokens.push(`lovelace: ${assets.lovelace.toString()}`)
186+
/**
187+
* Convert a core Value to the Assets interface format.
188+
*/
189+
export const valueToAssets = (value: CoreValue.Value): Assets => {
190+
const assets: Assets = { lovelace: 0n }
127191

128-
for (const [unit, amount] of Object.entries(assets)) {
129-
if (unit !== "lovelace") {
130-
tokens.push(`${unit}: ${amount.toString()}`)
192+
// Add ADA (lovelace) from the Value
193+
const adaAmount = CoreValue.getAda(value)
194+
assets.lovelace = BigInt(adaAmount.toString())
195+
196+
// Get MultiAsset if it exists
197+
const multiAsset = CoreValue.getAssets(value)
198+
if (Option.isSome(multiAsset)) {
199+
// Iterate through all policy IDs
200+
const policyIds = MultiAsset.getPolicyIds(multiAsset.value)
201+
202+
for (const policyId of policyIds) {
203+
const policyIdStr = CorePolicyId.toHex(policyId)
204+
const assetsByPolicy = MultiAsset.getAssetsByPolicy(multiAsset.value, policyId)
205+
206+
for (const [assetName, amount] of assetsByPolicy) {
207+
const assetNameStr = AssetName.toHex(assetName)
208+
const unit = policyIdStr + assetNameStr
209+
assets[unit] = BigInt(amount.toString())
210+
}
211+
}
212+
}
213+
214+
return assets
215+
}
216+
217+
/**
218+
* Convert Assets interface format to a core Value.
219+
*/
220+
export const assetsToValue = (assets: Assets): CoreValue.Value => {
221+
// Extract ADA amount (lovelace key)
222+
const adaAmount = assets.lovelace || BigInt(0)
223+
const coin = Coin.make(adaAmount)
224+
225+
// Filter out ADA to get only native assets
226+
const nativeAssets = Object.entries(assets).filter(([unit]) => unit !== "lovelace")
227+
228+
if (nativeAssets.length === 0) {
229+
// Only ADA, return OnlyCoin
230+
return CoreValue.onlyCoin(coin)
231+
}
232+
233+
// Build MultiAsset
234+
const multiAssetMap = MultiAsset.empty()
235+
236+
for (const [unit, amount] of nativeAssets) {
237+
const { assetName, policyId } = Unit.fromUnit(unit)
238+
const positiveAmount = PositiveCoin.make(amount)
239+
240+
// Create core policy ID from hex string
241+
const corePolicyId = CorePolicyId.fromHex(policyId)
242+
// Create core asset name from hex string (or empty if undefined)
243+
const coreAssetName = AssetName.fromHex(assetName || "")
244+
245+
// Get or create policy map
246+
let policyMap = multiAssetMap.get(corePolicyId)
247+
if (!policyMap) {
248+
policyMap = new Map()
249+
multiAssetMap.set(corePolicyId, policyMap)
131250
}
251+
252+
// Add asset to policy map
253+
policyMap.set(coreAssetName, positiveAmount)
132254
}
133255

134-
return `{ ${tokens.join(", ")} }`
256+
// Create the MultiAsset using the make function
257+
const multiAsset = MultiAsset.make(multiAssetMap)
258+
259+
return CoreValue.withAssets(coin, multiAsset)
135260
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { fromHex } from "../core/Bytes.js"
2+
3+
/**
4+
* CRC8 implementation for label checksum calculation.
5+
* Uses polynomial 0x07 (x^8 + x^2 + x + 1) as per CIP-67 specification.
6+
*/
7+
function crc8(data: Uint8Array): number {
8+
let crc = 0
9+
10+
for (let i = 0; i < data.length; i++) {
11+
crc ^= data[i]
12+
13+
for (let j = 0; j < 8; j++) {
14+
if (crc & 0x80) {
15+
crc = (crc << 1) ^ 0x07
16+
} else {
17+
crc = crc << 1
18+
}
19+
}
20+
}
21+
22+
return crc & 0xFF
23+
}
24+
25+
/**
26+
* Generate checksum for a hex-encoded number.
27+
*/
28+
function checksum(num: string): string {
29+
return crc8(fromHex(num)).toString(16).padStart(2, "0")
30+
}
31+
32+
/**
33+
* Convert a number to a CIP-67 label format.
34+
* Creates an 8-character hex string with format: 0[4-digit-hex][2-digit-checksum]0
35+
*/
36+
export function toLabel(num: number): string {
37+
if (num < 0 || num > 65535) {
38+
throw new Error(
39+
`Label ${num} out of range: min label 1 - max label 65535.`
40+
)
41+
}
42+
const numHex = num.toString(16).padStart(4, "0")
43+
return "0" + numHex + checksum(numHex) + "0"
44+
}
45+
46+
/**
47+
* Parse a CIP-67 label format back to a number.
48+
* Returns undefined if the label format is invalid or checksum doesn't match.
49+
*/
50+
export function fromLabel(label: string): number | undefined {
51+
if (label.length !== 8 || !(label[0] === "0" && label[7] === "0")) {
52+
return undefined
53+
}
54+
const numHex = label.slice(1, 5)
55+
const num = parseInt(numHex, 16)
56+
const check = label.slice(5, 7)
57+
return check === checksum(numHex) ? num : undefined
58+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export type PolicyId = string
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import * as CoreAddressEras from "../core/AddressEras.js"
2+
import * as CoreRewardAccount from "../core/RewardAccount.js"
3+
import * as Credential from "./Credential.js"
4+
5+
/**
6+
* Reward address in bech32 format.
7+
* Mainnet addresses start with "stake1"
8+
* Testnet addresses start with "stake_test1"
9+
*/
10+
export type RewardAddress = string
11+
12+
/**
13+
* Convert bech32 reward address to core RewardAccount structure
14+
*/
15+
export const toCoreRewardAccount = (address: RewardAddress): CoreRewardAccount.RewardAccount => {
16+
const addressEras = CoreAddressEras.fromBech32(address)
17+
if (addressEras._tag !== "RewardAccount") {
18+
throw new Error(`Invalid reward address: expected RewardAccount, got ${addressEras._tag}`)
19+
}
20+
return addressEras
21+
}
22+
23+
/**
24+
* Convert core RewardAccount structure to bech32 reward address
25+
*/
26+
export const fromCoreRewardAccount = (account: CoreRewardAccount.RewardAccount): RewardAddress =>
27+
CoreAddressEras.toBech32(account)
28+
29+
/**
30+
* Create a reward address from a stake credential.
31+
* A reward address is used for staking rewards and has only a stake credential.
32+
*
33+
* @param stakeCredential - The stake credential (key hash or script hash)
34+
* @param network - Target network (defaults to "Mainnet")
35+
* @returns Bech32 reward address string
36+
*
37+
* @example
38+
* ```typescript
39+
* const credential = { type: "Key", hash: "abcd1234..." }
40+
* const rewardAddr = fromStakeCredential(credential, "Mainnet")
41+
* // Returns: "stake1u9rlm65wjfkctdlyxl5cw87h9..."
42+
*
43+
* const scriptCredential = { type: "Script", hash: "def5678..." }
44+
* const testnetAddr = fromStakeCredential(scriptCredential, "Testnet")
45+
* // Returns: "stake_test1ur9rlm65wjfkctdlyxl5cw87h9..."
46+
* ```
47+
*/
48+
export const fromStakeCredential = (
49+
stakeCredential: Credential.Credential,
50+
network: Credential.Network = "Mainnet"
51+
): RewardAddress => {
52+
const coreRewardAccount = new CoreRewardAccount.RewardAccount({
53+
networkId: network === "Mainnet" ? 1 : 0,
54+
stakeCredential: Credential.toCoreCredential(stakeCredential)
55+
})
56+
return CoreAddressEras.toBech32(coreRewardAccount)
57+
}

packages/evolution/src/sdk/Script.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { pipe } from "effect"
22

3+
import { fromHex } from "../core/Bytes.js"
4+
import * as CBOR from "../core/CBOR.js"
35
import * as CoreScript from "../core/Script.js"
46
import * as CoreScriptHash from "../core/ScriptHash.js"
57
import type * as Credential from "./Credential.js"
@@ -110,3 +112,18 @@ export const scriptEquals = (a: Script, b: Script): boolean => a.type === b.type
110112
* The policy ID is identical to the script hash.
111113
*/
112114
export const mintingPolicyToId = (script: Script): Credential.ScriptHash => toScriptHash(script)
115+
116+
117+
export const applyDoubleCborEncoding = (script: string): string => {
118+
// Convert hex string to bytes, then encode as CBOR bytes, then back to hex
119+
const bytes = fromHex(script)
120+
const doubleCborBytes = CBOR.toCBORBytes(bytes)
121+
return CBOR.toCBORHex(doubleCborBytes)
122+
}
123+
124+
export const applySingleCborEncoding = (script: string): string => {
125+
// Convert hex string to bytes, then encode as CBOR bytes, then back to hex
126+
const bytes = fromHex(script)
127+
const singleCborBytes = CBOR.toCBORBytes(bytes)
128+
return CBOR.toCBORHex(singleCborBytes)
129+
}

packages/evolution/src/sdk/UTxO.ts

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
import * as Assets from "./Assets.js"
2+
import type * as Script from "./Script.js"
23

34
export type Datum =
45
| {
6+
type: "datumHash"
57
hash: string
68
}
79
| {
10+
type: "inlineDatum"
811
inline: string
912
}
10-
| undefined
13+
| {
14+
type: "noDatum"
15+
}
1116

1217
export interface OutRef {
1318
txHash: string
@@ -19,8 +24,8 @@ export interface UTxO {
1924
outputIndex: number
2025
address: string
2126
assets: Assets.Assets
22-
datumOption?: Datum
23-
scriptRef?: string
27+
datumOption: Datum
28+
scriptRef?: Script.Script
2429
}
2530

2631
export const hasAssets = (utxo: UTxO): boolean => !Assets.isEmpty(utxo.assets)
@@ -54,9 +59,9 @@ export const makeOutRef = (txHash: string, outputIndex: number): OutRef => ({
5459
})
5560

5661
// Datum type guards and utilities
57-
export const isDatumHash = (datum: Datum): datum is { hash: string } => datum !== undefined && "hash" in datum
62+
export const isDatumHash = (datum: Datum): datum is { type: "datumHash"; hash: string } => datum !== undefined && "hash" in datum
5863

59-
export const isInlineDatum = (datum: Datum): datum is { inline: string } => datum !== undefined && "inline" in datum
64+
export const isInlineDatum = (datum: Datum): datum is { type: "inlineDatum"; inline: string } => datum !== undefined && "inline" in datum
6065

6166
export const getDatumHash = (utxo: UTxO): string | undefined =>
6267
isDatumHash(utxo.datumOption) ? utxo.datumOption.hash : undefined
@@ -85,11 +90,13 @@ export const withDatum = (utxo: UTxO, datumOption: Datum): UTxO => ({
8590

8691
export const withoutDatum = (utxo: UTxO): UTxO => ({
8792
...utxo,
88-
datumOption: undefined
93+
datumOption: {
94+
type: "noDatum"
95+
}
8996
})
9097

9198
// Script operations
92-
export const withScript = (utxo: UTxO, scriptRef: string): UTxO => ({
99+
export const withScript = (utxo: UTxO, scriptRef: Script.Script): UTxO => ({
93100
...utxo,
94101
scriptRef
95102
})

0 commit comments

Comments
 (0)