Skip to content

Commit e290dae

Browse files
authored
feat: implement MLXDR operations bundle handling (#21)
1 parent aec0d2b commit e290dae

File tree

6 files changed

+401
-6
lines changed

6 files changed

+401
-6
lines changed

deno.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@moonlight/moonlight-sdk",
3-
"version": "0.5.0",
3+
"version": "0.6.0",
44
"description": "A privacy-focused toolkit for the Moonlight protocol on Stellar Soroban smart contracts.",
55
"license": "MIT",
66
"tasks": {

src/custom-xdr/index.ts

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,12 @@
1616
import { Buffer } from "buffer";
1717
import { MLXDRPrefix, MLXDRTypeByte } from "./types.ts";
1818
import type { Condition as ConditionType } from "../conditions/types.ts";
19-
import { nativeToScVal, scValToBigInt, xdr } from "@stellar/stellar-sdk";
19+
import {
20+
nativeToScVal,
21+
scValToBigInt,
22+
scValToNative,
23+
xdr,
24+
} from "@stellar/stellar-sdk";
2025
import { Condition } from "../conditions/index.ts";
2126
import {
2227
type MoonlightOperation as MoonlightOperationType,
@@ -38,6 +43,8 @@ const MLXDROperationBytes = [
3843
MLXDRTypeByte.WithdrawOperation,
3944
];
4045

46+
const MLXDROperationsBundleBytes = [MLXDRTypeByte.OperationsBundle];
47+
4148
const MLXDRTransactionBundleBytes = [MLXDRTypeByte.TransactionBundle];
4249

4350
const isMLXDR = (data: string): boolean => {
@@ -93,6 +100,13 @@ const isOperation = (data: string): boolean => {
93100
return MLXDROperationBytes.includes(prefixByte);
94101
};
95102

103+
const isOperationsBundle = (data: string): boolean => {
104+
const typePrefix = getMLXDRTypePrefix(data);
105+
106+
const prefixByte = typePrefix[0];
107+
return MLXDROperationsBundleBytes.includes(prefixByte);
108+
};
109+
96110
const isTransactionBundle = (data: string): boolean => {
97111
const typePrefix = getMLXDRTypePrefix(data);
98112

@@ -310,6 +324,52 @@ const MLXDRtoOperation = (data: string): MoonlightOperationType => {
310324
}
311325
};
312326

327+
const operationsBundleToMLXDR = (
328+
operations: MoonlightOperationType[],
329+
): string => {
330+
if (operations.length === 0) {
331+
throw new Error("Operations bundle cannot be empty");
332+
}
333+
const operationMLXDRArray = operations.map((op) => {
334+
return xdr.ScVal.scvString(op.toMLXDR());
335+
});
336+
const typeByte: MLXDRTypeByte = MLXDRTypeByte.OperationsBundle;
337+
338+
const operationBundleXDR = xdr.ScVal.scvVec([
339+
...operationMLXDRArray,
340+
]).toXDR("base64");
341+
342+
return appendMLXDRPrefixToRawXDR(operationBundleXDR, typeByte);
343+
};
344+
345+
const MLXDRtoOperationsBundle = (data: string): MoonlightOperationType[] => {
346+
if (!isOperationsBundle(data)) {
347+
throw new Error("Data is not a valid MLXDR Operations Bundle");
348+
}
349+
350+
const buffer = Buffer.from(data, "base64");
351+
const rawXDRBuffer = buffer.slice(3);
352+
const rawXDRString = rawXDRBuffer.toString("base64");
353+
354+
const scVal = xdr.ScVal.fromXDR(rawXDRString, "base64");
355+
356+
const vec = scVal.vec();
357+
358+
if (vec === null) {
359+
throw new Error("Invalid ScVal vector for operations bundle");
360+
}
361+
362+
const operations: MoonlightOperationType[] = vec.map((opScVal) => {
363+
if (opScVal.switch().name !== xdr.ScValType.scvString().name) {
364+
throw new Error("Invalid ScVal type for operation in bundle");
365+
}
366+
const opMLXDR = scValToNative(opScVal) as string;
367+
return MLXDRtoOperation(opMLXDR);
368+
});
369+
370+
return operations;
371+
};
372+
313373
/**
314374
* * MLXDR Module
315375
*
@@ -330,10 +390,13 @@ export const MLXDR = {
330390
is: isMLXDR,
331391
isCondition,
332392
isOperation,
393+
isOperationsBundle,
333394
isTransactionBundle,
334395
getXDRType,
335396
fromCondition: conditionToMLXDR,
336397
toCondition: MLXDRtoCondition,
337398
fromOperation: operationToMLXDR,
338399
toOperation: MLXDRtoOperation,
400+
fromOperationsBundle: operationsBundleToMLXDR,
401+
toOperationsBundle: MLXDRtoOperationsBundle,
339402
};

src/custom-xdr/index.unit.test.ts

Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
import { assert, assertEquals, assertExists } from "@std/assert";
2+
import { beforeAll, describe, it } from "@std/testing/bdd";
3+
import {
4+
type ContractId,
5+
type Ed25519PublicKey,
6+
LocalSigner,
7+
} from "@colibri/core";
8+
import type { UTXOPublicKey } from "../core/utxo-keypair-base/types.ts";
9+
import { generateP256KeyPair } from "../utils/secp256r1/generateP256KeyPair.ts";
10+
11+
import { Asset, Networks } from "@stellar/stellar-sdk";
12+
13+
import {
14+
type CreateOperation,
15+
type DepositOperation,
16+
type SpendOperation,
17+
UTXOOperationType,
18+
type WithdrawOperation,
19+
} from "../operation/types.ts";
20+
import { MoonlightOperation } from "../operation/index.ts";
21+
import { UTXOKeypairBase } from "../core/utxo-keypair-base/index.ts";
22+
import { MLXDR } from "./index.ts";
23+
24+
describe("MLXDR", () => {
25+
let validPublicKey: Ed25519PublicKey;
26+
let validUtxo: UTXOPublicKey;
27+
28+
let channelId: ContractId;
29+
30+
let assetId: ContractId;
31+
let network: string;
32+
33+
beforeAll(async () => {
34+
validPublicKey = LocalSigner.generateRandom()
35+
.publicKey() as Ed25519PublicKey;
36+
validUtxo = (await generateP256KeyPair()).publicKey as UTXOPublicKey;
37+
38+
channelId =
39+
"CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC" as ContractId;
40+
41+
network = Networks.TESTNET;
42+
assetId = Asset.native().contractId(network) as ContractId;
43+
});
44+
45+
describe("OperationsBundle MLXDR", () => {
46+
it("should verify if the Operations Bundle is a valid MLXDR", () => {
47+
const createOp = MoonlightOperation.create(validUtxo, 10n);
48+
49+
const opBundleMLXDR = MLXDR.fromOperationsBundle([createOp]);
50+
51+
assertEquals(MLXDR.isOperationsBundle(opBundleMLXDR), true);
52+
});
53+
it("should convert to and from one operation", () => {
54+
const createOp = MoonlightOperation.create(validUtxo, 10n);
55+
56+
const opBundleMLXDR = MLXDR.fromOperationsBundle([createOp]);
57+
58+
const recreatedOps = MLXDR.toOperationsBundle(opBundleMLXDR);
59+
60+
assertEquals(recreatedOps.length, 1);
61+
assertEquals(recreatedOps[0].getOperation(), UTXOOperationType.CREATE);
62+
assertEquals(recreatedOps[0].getAmount(), 10n);
63+
assert(recreatedOps[0].isCreate());
64+
65+
assertEquals(
66+
(recreatedOps[0] as CreateOperation).getUtxo().toString(),
67+
validUtxo.toString(),
68+
);
69+
});
70+
71+
it("should convert to and from multiple Operations", () => {
72+
const createOpA = MoonlightOperation.create(validUtxo, 10n);
73+
const createOpB = MoonlightOperation.create(validUtxo, 20n);
74+
const createOpC = MoonlightOperation.create(validUtxo, 30n);
75+
76+
const opBundleMLXDR = MLXDR.fromOperationsBundle([
77+
createOpA,
78+
createOpB,
79+
createOpC,
80+
]);
81+
82+
const recreatedOps = MLXDR.toOperationsBundle(opBundleMLXDR);
83+
84+
assertEquals(recreatedOps.length, 3);
85+
86+
assertEquals(recreatedOps[0].getOperation(), UTXOOperationType.CREATE);
87+
assertEquals(recreatedOps[0].getAmount(), 10n);
88+
assert(recreatedOps[0].isCreate());
89+
assertEquals(
90+
(recreatedOps[0] as CreateOperation).getUtxo().toString(),
91+
validUtxo.toString(),
92+
);
93+
94+
assertEquals(recreatedOps[1].getOperation(), UTXOOperationType.CREATE);
95+
assertEquals(recreatedOps[1].getAmount(), 20n);
96+
assert(recreatedOps[1].isCreate());
97+
assertEquals(
98+
(recreatedOps[1] as CreateOperation).getUtxo().toString(),
99+
validUtxo.toString(),
100+
);
101+
102+
assertEquals(recreatedOps[2].getOperation(), UTXOOperationType.CREATE);
103+
assertEquals(recreatedOps[2].getAmount(), 30n);
104+
assert(recreatedOps[2].isCreate());
105+
assertEquals(
106+
(recreatedOps[2] as CreateOperation).getUtxo().toString(),
107+
validUtxo.toString(),
108+
);
109+
});
110+
111+
it("should convert to and from multiple mixed operations", () => {
112+
const createOp = MoonlightOperation.create(validUtxo, 10n);
113+
const depositOp = MoonlightOperation.deposit(validPublicKey, 20n);
114+
const withdrawOp = MoonlightOperation.withdraw(validPublicKey, 30n);
115+
const spendOp = MoonlightOperation.spend(validUtxo);
116+
117+
const opBundleMLXDR = MLXDR.fromOperationsBundle([
118+
createOp,
119+
depositOp,
120+
withdrawOp,
121+
spendOp,
122+
]);
123+
124+
const recreatedOps = MLXDR.toOperationsBundle(opBundleMLXDR);
125+
126+
assertEquals(recreatedOps.length, 4);
127+
128+
assertEquals(recreatedOps[0].getOperation(), UTXOOperationType.CREATE);
129+
assertEquals(recreatedOps[0].getAmount(), 10n);
130+
assert(recreatedOps[0].isCreate());
131+
assertEquals(
132+
(recreatedOps[0] as CreateOperation).getUtxo().toString(),
133+
validUtxo.toString(),
134+
);
135+
136+
assertEquals(recreatedOps[1].getOperation(), UTXOOperationType.DEPOSIT);
137+
assertEquals(recreatedOps[1].getAmount(), 20n);
138+
assert(recreatedOps[1].isDeposit());
139+
assertEquals(
140+
(recreatedOps[1] as DepositOperation).getPublicKey().toString(),
141+
validPublicKey.toString(),
142+
);
143+
144+
assertEquals(recreatedOps[2].getOperation(), UTXOOperationType.WITHDRAW);
145+
assertEquals(recreatedOps[2].getAmount(), 30n);
146+
assert(recreatedOps[2].isWithdraw());
147+
assertEquals(
148+
(recreatedOps[2] as WithdrawOperation).getPublicKey().toString(),
149+
validPublicKey.toString(),
150+
);
151+
152+
assertEquals(recreatedOps[3].getOperation(), UTXOOperationType.SPEND);
153+
assert(recreatedOps[3].isSpend());
154+
assertEquals(
155+
(recreatedOps[3] as SpendOperation).getUtxo().toString(),
156+
validUtxo.toString(),
157+
);
158+
});
159+
160+
it("should handle signed operations in an Operation Bundle", async () => {
161+
const userSigner = LocalSigner.generateRandom();
162+
const spender = new UTXOKeypairBase(await generateP256KeyPair());
163+
const createOp = MoonlightOperation.create(validUtxo, 1000n);
164+
165+
const depositOp = await MoonlightOperation.deposit(
166+
userSigner.publicKey() as Ed25519PublicKey,
167+
1000n,
168+
).addCondition(createOp.toCondition()).signWithEd25519(
169+
userSigner,
170+
100000,
171+
channelId,
172+
assetId,
173+
network,
174+
);
175+
176+
const spendOp = await MoonlightOperation.spend(
177+
spender.publicKey,
178+
).addCondition(createOp.toCondition()).signWithUTXO(
179+
spender,
180+
channelId,
181+
1000,
182+
);
183+
184+
const withdrawOp = await MoonlightOperation.withdraw(
185+
userSigner.publicKey() as Ed25519PublicKey,
186+
500n,
187+
).addCondition(createOp.toCondition());
188+
189+
const opBundleMLXDR = MLXDR.fromOperationsBundle([
190+
createOp,
191+
depositOp,
192+
withdrawOp,
193+
spendOp,
194+
]);
195+
196+
const recreatedOps = MLXDR.toOperationsBundle(opBundleMLXDR);
197+
198+
assertEquals(recreatedOps.length, 4);
199+
200+
// Validate create Operation Signature
201+
assertEquals(recreatedOps[0].getOperation(), UTXOOperationType.CREATE);
202+
assertEquals(recreatedOps[0].getAmount(), 1000n);
203+
assert(recreatedOps[0].isCreate());
204+
assertEquals(
205+
(recreatedOps[0] as CreateOperation).getUtxo().toString(),
206+
validUtxo.toString(),
207+
);
208+
209+
// Validate deposit Operation Signature
210+
assertEquals(recreatedOps[1].getOperation(), UTXOOperationType.DEPOSIT);
211+
assertEquals(recreatedOps[1].getAmount(), 1000n);
212+
assert(recreatedOps[1].isDeposit());
213+
assertEquals(
214+
(recreatedOps[1] as DepositOperation).getPublicKey().toString(),
215+
userSigner.publicKey().toString(),
216+
);
217+
assertEquals(
218+
(recreatedOps[1] as DepositOperation).isSignedByEd25519(),
219+
true,
220+
);
221+
assertExists(
222+
(recreatedOps[1] as DepositOperation).getEd25519Signature(),
223+
);
224+
assertEquals(
225+
(recreatedOps[1] as DepositOperation).getConditions(),
226+
depositOp.getConditions(),
227+
);
228+
// Validate withdraw Operation Signature
229+
assertEquals(recreatedOps[2].getOperation(), UTXOOperationType.WITHDRAW);
230+
assertEquals(recreatedOps[2].getAmount(), 500n);
231+
assert(recreatedOps[2].isWithdraw());
232+
assertEquals(
233+
(recreatedOps[2] as WithdrawOperation).getPublicKey().toString(),
234+
userSigner.publicKey().toString(),
235+
);
236+
237+
assertEquals(
238+
(recreatedOps[2] as WithdrawOperation).getConditions(),
239+
withdrawOp.getConditions(),
240+
);
241+
// Validate spend Operation Signature
242+
assertEquals(recreatedOps[3].getOperation(), UTXOOperationType.SPEND);
243+
assert(recreatedOps[3].isSpend());
244+
assertEquals(
245+
(recreatedOps[3] as SpendOperation).getUtxo().toString(),
246+
spender.publicKey.toString(),
247+
);
248+
assertEquals(
249+
(recreatedOps[3] as SpendOperation).isSignedByUTXO(),
250+
true,
251+
);
252+
assertExists(
253+
(recreatedOps[3] as SpendOperation).getUTXOSignature(),
254+
);
255+
assertEquals(
256+
(recreatedOps[3] as SpendOperation).getConditions(),
257+
spendOp.getConditions(),
258+
);
259+
});
260+
});
261+
});

src/custom-xdr/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ export enum MLXDRTypeByte {
88
SpendOperation = 0x05,
99
DepositOperation = 0x06,
1010
WithdrawOperation = 0x07,
11-
12-
TransactionBundle = 0x08,
11+
OperationsBundle = 0x08,
12+
TransactionBundle = 0x09,
1313
}
1414

1515
export const MLXDRPrefix: Buffer = Buffer.from([0x30, 0xb0]);

src/operation/index.unit.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ import { UTXOOperationType } from "./types.ts";
1212
import type { CreateCondition } from "../conditions/types.ts";
1313
import { Condition } from "../conditions/index.ts";
1414
import { Asset, Networks } from "@stellar/stellar-sdk";
15-
import { UTXOKeypairBase } from "@moonlight/moonlight-sdk";
15+
import { UTXOKeypairBase } from "../core/utxo-keypair-base/index.ts";
1616

17-
describe("Condition", () => {
17+
describe("Operation", () => {
1818
let validPublicKey: Ed25519PublicKey;
1919
let validUtxo: UTXOPublicKey;
2020

0 commit comments

Comments
 (0)