Skip to content

Commit 6084fda

Browse files
committed
feat: implement MLXDR operations bundle handling
- Added support for operations bundles in MLXDR by introducing new methods: - `fromOperationsBundle`: Converts an array of Moonlight operations to an MLXDR operations bundle. - `toOperationsBundle`: Converts an MLXDR operations bundle back to an array of Moonlight operations. - Added validation for operations bundles in `isOperationsBundle`. - Updated `MLXDRTypeByte` enum to include `OperationsBundle` and adjusted `TransactionBundle` value. - Created unit tests for operations bundle functionality in `index.unit.test.ts` to ensure correct conversion and validation of operations bundles. - Refactored imports in `index.unit.test.ts` and `signing.unit.test.ts` for consistency and clarity.
1 parent aec0d2b commit 6084fda

File tree

5 files changed

+413
-5
lines changed

5 files changed

+413
-5
lines changed

src/custom-xdr/index.ts

Lines changed: 61 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,49 @@ const MLXDRtoOperation = (data: string): MoonlightOperationType => {
310324
}
311325
};
312326

327+
const operationsBundleToMLXDR = (
328+
operations: MoonlightOperationType[],
329+
): string => {
330+
const operationMLXDRArray = operations.map((op) => {
331+
return xdr.ScVal.scvString(op.toMLXDR());
332+
});
333+
const typeByte: MLXDRTypeByte = MLXDRTypeByte.OperationsBundle;
334+
335+
const operationBundleXDR = xdr.ScVal.scvVec([
336+
...operationMLXDRArray,
337+
]).toXDR("base64");
338+
339+
return appendMLXDRPrefixToRawXDR(operationBundleXDR, typeByte);
340+
};
341+
342+
const MLXDRtoOperationsBundle = (data: string): MoonlightOperationType[] => {
343+
if (!isOperationsBundle(data)) {
344+
throw new Error("Data is not a valid MLXDR Operations Bundle");
345+
}
346+
347+
const buffer = Buffer.from(data, "base64");
348+
const rawXDRBuffer = buffer.slice(3);
349+
const rawXDRString = rawXDRBuffer.toString("base64");
350+
351+
const scVal = xdr.ScVal.fromXDR(rawXDRString, "base64");
352+
353+
const vec = scVal.vec();
354+
355+
if (vec === null) {
356+
throw new Error("Invalid ScVal vector for operations bundle");
357+
}
358+
359+
const operations: MoonlightOperationType[] = vec.map((opScVal) => {
360+
if (opScVal.switch().name !== xdr.ScValType.scvString().name) {
361+
throw new Error("Invalid ScVal type for operation in bundle");
362+
}
363+
const opMLXDR = scValToNative(opScVal) as string;
364+
return MLXDRtoOperation(opMLXDR);
365+
});
366+
367+
return operations;
368+
};
369+
313370
/**
314371
* * MLXDR Module
315372
*
@@ -330,10 +387,13 @@ export const MLXDR = {
330387
is: isMLXDR,
331388
isCondition,
332389
isOperation,
390+
isOperationsBundle,
333391
isTransactionBundle,
334392
getXDRType,
335393
fromCondition: conditionToMLXDR,
336394
toCondition: MLXDRtoCondition,
337395
fromOperation: operationToMLXDR,
338396
toOperation: MLXDRtoOperation,
397+
fromOperationsBundle: operationsBundleToMLXDR,
398+
toOperationsBundle: MLXDRtoOperationsBundle,
339399
};

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

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

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)