Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion deno.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@moonlight/moonlight-sdk",
"version": "0.5.0",
"version": "0.6.0",
"description": "A privacy-focused toolkit for the Moonlight protocol on Stellar Soroban smart contracts.",
"license": "MIT",
"tasks": {
Expand Down
65 changes: 64 additions & 1 deletion src/custom-xdr/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,12 @@
import { Buffer } from "buffer";
import { MLXDRPrefix, MLXDRTypeByte } from "./types.ts";
import type { Condition as ConditionType } from "../conditions/types.ts";
import { nativeToScVal, scValToBigInt, xdr } from "@stellar/stellar-sdk";
import {
nativeToScVal,
scValToBigInt,
scValToNative,
xdr,
} from "@stellar/stellar-sdk";
import { Condition } from "../conditions/index.ts";
import {
type MoonlightOperation as MoonlightOperationType,
Expand All @@ -38,6 +43,8 @@ const MLXDROperationBytes = [
MLXDRTypeByte.WithdrawOperation,
];

const MLXDROperationsBundleBytes = [MLXDRTypeByte.OperationsBundle];

const MLXDRTransactionBundleBytes = [MLXDRTypeByte.TransactionBundle];

const isMLXDR = (data: string): boolean => {
Expand Down Expand Up @@ -93,6 +100,13 @@ const isOperation = (data: string): boolean => {
return MLXDROperationBytes.includes(prefixByte);
};

const isOperationsBundle = (data: string): boolean => {
const typePrefix = getMLXDRTypePrefix(data);

const prefixByte = typePrefix[0];
return MLXDROperationsBundleBytes.includes(prefixByte);
};

const isTransactionBundle = (data: string): boolean => {
const typePrefix = getMLXDRTypePrefix(data);

Expand Down Expand Up @@ -310,6 +324,52 @@ const MLXDRtoOperation = (data: string): MoonlightOperationType => {
}
};

const operationsBundleToMLXDR = (
operations: MoonlightOperationType[],
): string => {
if (operations.length === 0) {
throw new Error("Operations bundle cannot be empty");
}
const operationMLXDRArray = operations.map((op) => {
return xdr.ScVal.scvString(op.toMLXDR());
});
const typeByte: MLXDRTypeByte = MLXDRTypeByte.OperationsBundle;

const operationBundleXDR = xdr.ScVal.scvVec([
...operationMLXDRArray,
]).toXDR("base64");

return appendMLXDRPrefixToRawXDR(operationBundleXDR, typeByte);
};

const MLXDRtoOperationsBundle = (data: string): MoonlightOperationType[] => {
if (!isOperationsBundle(data)) {
throw new Error("Data is not a valid MLXDR Operations Bundle");
}

const buffer = Buffer.from(data, "base64");
const rawXDRBuffer = buffer.slice(3);
const rawXDRString = rawXDRBuffer.toString("base64");

const scVal = xdr.ScVal.fromXDR(rawXDRString, "base64");

const vec = scVal.vec();

if (vec === null) {
throw new Error("Invalid ScVal vector for operations bundle");
}

const operations: MoonlightOperationType[] = vec.map((opScVal) => {
if (opScVal.switch().name !== xdr.ScValType.scvString().name) {
throw new Error("Invalid ScVal type for operation in bundle");
}
const opMLXDR = scValToNative(opScVal) as string;
return MLXDRtoOperation(opMLXDR);
});

return operations;
};

/**
* * MLXDR Module
*
Expand All @@ -330,10 +390,13 @@ export const MLXDR = {
is: isMLXDR,
isCondition,
isOperation,
isOperationsBundle,
isTransactionBundle,
getXDRType,
fromCondition: conditionToMLXDR,
toCondition: MLXDRtoCondition,
fromOperation: operationToMLXDR,
toOperation: MLXDRtoOperation,
fromOperationsBundle: operationsBundleToMLXDR,
toOperationsBundle: MLXDRtoOperationsBundle,
};
261 changes: 261 additions & 0 deletions src/custom-xdr/index.unit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,261 @@
import { assert, assertEquals, assertExists } from "@std/assert";
import { beforeAll, describe, it } from "@std/testing/bdd";
import {
type ContractId,
type Ed25519PublicKey,
LocalSigner,
} from "@colibri/core";
import type { UTXOPublicKey } from "../core/utxo-keypair-base/types.ts";
import { generateP256KeyPair } from "../utils/secp256r1/generateP256KeyPair.ts";

import { Asset, Networks } from "@stellar/stellar-sdk";

import {
type CreateOperation,
type DepositOperation,
type SpendOperation,
UTXOOperationType,
type WithdrawOperation,
} from "../operation/types.ts";
import { MoonlightOperation } from "../operation/index.ts";
import { UTXOKeypairBase } from "../core/utxo-keypair-base/index.ts";
import { MLXDR } from "./index.ts";

describe("MLXDR", () => {
let validPublicKey: Ed25519PublicKey;
let validUtxo: UTXOPublicKey;

let channelId: ContractId;

let assetId: ContractId;
let network: string;

beforeAll(async () => {
validPublicKey = LocalSigner.generateRandom()
.publicKey() as Ed25519PublicKey;
validUtxo = (await generateP256KeyPair()).publicKey as UTXOPublicKey;

channelId =
"CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC" as ContractId;

network = Networks.TESTNET;
assetId = Asset.native().contractId(network) as ContractId;
});

describe("OperationsBundle MLXDR", () => {
it("should verify if the Operations Bundle is a valid MLXDR", () => {
const createOp = MoonlightOperation.create(validUtxo, 10n);

const opBundleMLXDR = MLXDR.fromOperationsBundle([createOp]);

assertEquals(MLXDR.isOperationsBundle(opBundleMLXDR), true);
});
it("should convert to and from one operation", () => {
const createOp = MoonlightOperation.create(validUtxo, 10n);

const opBundleMLXDR = MLXDR.fromOperationsBundle([createOp]);

const recreatedOps = MLXDR.toOperationsBundle(opBundleMLXDR);

assertEquals(recreatedOps.length, 1);
assertEquals(recreatedOps[0].getOperation(), UTXOOperationType.CREATE);
assertEquals(recreatedOps[0].getAmount(), 10n);
assert(recreatedOps[0].isCreate());

assertEquals(
(recreatedOps[0] as CreateOperation).getUtxo().toString(),
validUtxo.toString(),
);
});

it("should convert to and from multiple Operations", () => {
const createOpA = MoonlightOperation.create(validUtxo, 10n);
const createOpB = MoonlightOperation.create(validUtxo, 20n);
const createOpC = MoonlightOperation.create(validUtxo, 30n);

const opBundleMLXDR = MLXDR.fromOperationsBundle([
createOpA,
createOpB,
createOpC,
]);

const recreatedOps = MLXDR.toOperationsBundle(opBundleMLXDR);

assertEquals(recreatedOps.length, 3);

assertEquals(recreatedOps[0].getOperation(), UTXOOperationType.CREATE);
assertEquals(recreatedOps[0].getAmount(), 10n);
assert(recreatedOps[0].isCreate());
assertEquals(
(recreatedOps[0] as CreateOperation).getUtxo().toString(),
validUtxo.toString(),
);

assertEquals(recreatedOps[1].getOperation(), UTXOOperationType.CREATE);
assertEquals(recreatedOps[1].getAmount(), 20n);
assert(recreatedOps[1].isCreate());
assertEquals(
(recreatedOps[1] as CreateOperation).getUtxo().toString(),
validUtxo.toString(),
);

assertEquals(recreatedOps[2].getOperation(), UTXOOperationType.CREATE);
assertEquals(recreatedOps[2].getAmount(), 30n);
assert(recreatedOps[2].isCreate());
assertEquals(
(recreatedOps[2] as CreateOperation).getUtxo().toString(),
validUtxo.toString(),
);
});

it("should convert to and from multiple mixed operations", () => {
const createOp = MoonlightOperation.create(validUtxo, 10n);
const depositOp = MoonlightOperation.deposit(validPublicKey, 20n);
const withdrawOp = MoonlightOperation.withdraw(validPublicKey, 30n);
const spendOp = MoonlightOperation.spend(validUtxo);

const opBundleMLXDR = MLXDR.fromOperationsBundle([
createOp,
depositOp,
withdrawOp,
spendOp,
]);

const recreatedOps = MLXDR.toOperationsBundle(opBundleMLXDR);

assertEquals(recreatedOps.length, 4);

assertEquals(recreatedOps[0].getOperation(), UTXOOperationType.CREATE);
assertEquals(recreatedOps[0].getAmount(), 10n);
assert(recreatedOps[0].isCreate());
assertEquals(
(recreatedOps[0] as CreateOperation).getUtxo().toString(),
validUtxo.toString(),
);

assertEquals(recreatedOps[1].getOperation(), UTXOOperationType.DEPOSIT);
assertEquals(recreatedOps[1].getAmount(), 20n);
assert(recreatedOps[1].isDeposit());
assertEquals(
(recreatedOps[1] as DepositOperation).getPublicKey().toString(),
validPublicKey.toString(),
);

assertEquals(recreatedOps[2].getOperation(), UTXOOperationType.WITHDRAW);
assertEquals(recreatedOps[2].getAmount(), 30n);
assert(recreatedOps[2].isWithdraw());
assertEquals(
(recreatedOps[2] as WithdrawOperation).getPublicKey().toString(),
validPublicKey.toString(),
);

assertEquals(recreatedOps[3].getOperation(), UTXOOperationType.SPEND);
assert(recreatedOps[3].isSpend());
assertEquals(
(recreatedOps[3] as SpendOperation).getUtxo().toString(),
validUtxo.toString(),
);
});

it("should handle signed operations in an Operation Bundle", async () => {
const userSigner = LocalSigner.generateRandom();
const spender = new UTXOKeypairBase(await generateP256KeyPair());
const createOp = MoonlightOperation.create(validUtxo, 1000n);

const depositOp = await MoonlightOperation.deposit(
userSigner.publicKey() as Ed25519PublicKey,
1000n,
).addCondition(createOp.toCondition()).signWithEd25519(
userSigner,
100000,
channelId,
assetId,
network,
);

const spendOp = await MoonlightOperation.spend(
spender.publicKey,
).addCondition(createOp.toCondition()).signWithUTXO(
spender,
channelId,
1000,
);

const withdrawOp = await MoonlightOperation.withdraw(
userSigner.publicKey() as Ed25519PublicKey,
500n,
).addCondition(createOp.toCondition());

const opBundleMLXDR = MLXDR.fromOperationsBundle([
createOp,
depositOp,
withdrawOp,
spendOp,
]);

const recreatedOps = MLXDR.toOperationsBundle(opBundleMLXDR);

assertEquals(recreatedOps.length, 4);

// Validate create Operation Signature
assertEquals(recreatedOps[0].getOperation(), UTXOOperationType.CREATE);
assertEquals(recreatedOps[0].getAmount(), 1000n);
assert(recreatedOps[0].isCreate());
assertEquals(
(recreatedOps[0] as CreateOperation).getUtxo().toString(),
validUtxo.toString(),
);

// Validate deposit Operation Signature
assertEquals(recreatedOps[1].getOperation(), UTXOOperationType.DEPOSIT);
assertEquals(recreatedOps[1].getAmount(), 1000n);
assert(recreatedOps[1].isDeposit());
assertEquals(
(recreatedOps[1] as DepositOperation).getPublicKey().toString(),
userSigner.publicKey().toString(),
);
assertEquals(
(recreatedOps[1] as DepositOperation).isSignedByEd25519(),
true,
);
assertExists(
(recreatedOps[1] as DepositOperation).getEd25519Signature(),
);
assertEquals(
(recreatedOps[1] as DepositOperation).getConditions(),
depositOp.getConditions(),
);
// Validate withdraw Operation Signature
assertEquals(recreatedOps[2].getOperation(), UTXOOperationType.WITHDRAW);
assertEquals(recreatedOps[2].getAmount(), 500n);
assert(recreatedOps[2].isWithdraw());
assertEquals(
(recreatedOps[2] as WithdrawOperation).getPublicKey().toString(),
userSigner.publicKey().toString(),
);

assertEquals(
(recreatedOps[2] as WithdrawOperation).getConditions(),
withdrawOp.getConditions(),
);
// Validate spend Operation Signature
assertEquals(recreatedOps[3].getOperation(), UTXOOperationType.SPEND);
assert(recreatedOps[3].isSpend());
assertEquals(
(recreatedOps[3] as SpendOperation).getUtxo().toString(),
spender.publicKey.toString(),
);
assertEquals(
(recreatedOps[3] as SpendOperation).isSignedByUTXO(),
true,
);
assertExists(
(recreatedOps[3] as SpendOperation).getUTXOSignature(),
);
assertEquals(
(recreatedOps[3] as SpendOperation).getConditions(),
spendOp.getConditions(),
);
});
});
});
4 changes: 2 additions & 2 deletions src/custom-xdr/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ export enum MLXDRTypeByte {
SpendOperation = 0x05,
DepositOperation = 0x06,
WithdrawOperation = 0x07,

TransactionBundle = 0x08,
OperationsBundle = 0x08,
TransactionBundle = 0x09,
}

export const MLXDRPrefix: Buffer = Buffer.from([0x30, 0xb0]);
Expand Down
4 changes: 2 additions & 2 deletions src/operation/index.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ import { UTXOOperationType } from "./types.ts";
import type { CreateCondition } from "../conditions/types.ts";
import { Condition } from "../conditions/index.ts";
import { Asset, Networks } from "@stellar/stellar-sdk";
import { UTXOKeypairBase } from "@moonlight/moonlight-sdk";
import { UTXOKeypairBase } from "../core/utxo-keypair-base/index.ts";

describe("Condition", () => {
describe("Operation", () => {
let validPublicKey: Ed25519PublicKey;
let validUtxo: UTXOPublicKey;

Expand Down
Loading