import * as nacl from 'tweetnacl';
import bs58 from 'bs58';
// ============================================================================
// Enum Discriminant Mappings
// ============================================================================
const ACTION_CODES: Record<string, number> = {
order: 0,
oracle: 1,
faucet: 2,
updateUserSettings: 3,
agentWalletCreation: 4,
};
const ORDER_ITEM_CODES: Record<string, number> = {
order: 0,
cancel: 1,
cancelAll: 2,
};
const TIME_IN_FORCE_CODES: Record<string, number> = {
GTC: 0,
IOC: 1,
ALO: 2,
};
const ORDER_TYPE_CODES: Record<string, number> = {
limit: 0,
trigger: 1,
};
// ============================================================================
// Primitive Writers
// ============================================================================
function writeU32(value: number): Uint8Array {
const buf = new Uint8Array(4);
new DataView(buf.buffer).setUint32(0, value, true); // little-endian
return buf;
}
function writeU64(value: number | bigint): Uint8Array {
const buf = new Uint8Array(8);
new DataView(buf.buffer).setBigUint64(0, BigInt(value), true); // little-endian
return buf;
}
function writeBool(value: boolean): Uint8Array {
return new Uint8Array([value ? 1 : 0]);
}
function writeString(str: string): Uint8Array {
const bytes = new TextEncoder().encode(str);
return concatBytes(writeU64(bytes.length), bytes);
}
function writeF64(value: number): Uint8Array {
const buf = new Uint8Array(8);
new DataView(buf.buffer).setFloat64(0, value, true); // little-endian
return buf;
}
function concatBytes(...arrays: Uint8Array[]): Uint8Array {
const totalLength = arrays.reduce((sum, arr) => sum + arr.length, 0);
const result = new Uint8Array(totalLength);
let offset = 0;
for (const arr of arrays) {
result.set(arr, offset);
offset += arr.length;
}
return result;
}
// ============================================================================
// Validation Helpers
// ============================================================================
function decodeAndValidateKey(key: string): Uint8Array {
const bytes = bs58.decode(key);
if (bytes.length !== 32) {
throw new Error(`Key must be 32 bytes, got ${bytes.length}`);
}
return bytes;
}
function decodeAndValidateHash(hash: string): Uint8Array {
const bytes = bs58.decode(hash);
if (bytes.length !== 32) {
throw new Error(`Hash must be 32 bytes, got ${bytes.length}`);
}
return bytes;
}
// ============================================================================
// Order Serialization
// ============================================================================
function serializeOrderItem(orderWrapper: any): Uint8Array {
// Get the order item type (order, cancel, or cancelAll)
const itemType = Object.keys(orderWrapper)[0];
if (!itemType || !(itemType in ORDER_ITEM_CODES)) {
throw new Error(`Invalid order item type: ${itemType}`);
}
const parts: Uint8Array[] = [];
// Write order item discriminant (u32)
parts.push(writeU32(ORDER_ITEM_CODES[itemType]));
const data = orderWrapper[itemType];
if (itemType === 'order') {
// Order: asset, is_buy, price, size, reduce_only, order_type, client_id
parts.push(writeString(data.c)); // asset
parts.push(writeBool(data.b)); // is_buy
parts.push(writeF64(data.px)); // price
parts.push(writeF64(data.sz)); // size
parts.push(writeBool(data.r)); // reduce_only
// Order type (limit or trigger)
if (data.t.limit) {
parts.push(writeU32(ORDER_TYPE_CODES.limit));
parts.push(writeU32(TIME_IN_FORCE_CODES[data.t.limit.tif] || 0));
} else if (data.t.trigger) {
parts.push(writeU32(ORDER_TYPE_CODES.trigger));
parts.push(writeBool(data.t.trigger.is_market));
parts.push(writeF64(data.t.trigger.triggerPx));
} else {
throw new Error('Order must have either limit or trigger type');
}
// client_id: Option<Hash> - 1 byte discriminant + 32 bytes if Some
if (data.cloid) {
parts.push(writeBool(true));
parts.push(decodeAndValidateHash(data.cloid)); // Raw 32 bytes
} else {
parts.push(writeBool(false));
}
} else if (itemType === 'cancel') {
// Cancel: asset, oid (Hash as raw 32 bytes)
parts.push(writeString(data.c)); // asset
parts.push(decodeAndValidateHash(data.oid)); // oid - raw 32 bytes
} else if (itemType === 'cancelAll') {
// CancelAll: assets (Vec<String>)
const assets = data.c || [];
parts.push(writeU64(assets.length));
for (const asset of assets) {
parts.push(writeString(asset));
}
}
return concatBytes(...parts);
}
function serializeOrders(orders: any[]): Uint8Array {
const parts: Uint8Array[] = [];
// Write order count (u64)
parts.push(writeU64(orders.length));
// Serialize each order item
for (const order of orders) {
parts.push(serializeOrderItem(order));
}
return concatBytes(...parts);
}
// ============================================================================
// Action Serialization
// ============================================================================
function serializeFaucet(faucet: any): Uint8Array {
const parts: Uint8Array[] = [];
// user: Pubkey (raw 32 bytes)
parts.push(decodeAndValidateKey(faucet.u));
// amount: Option<f64>
if (faucet.amount !== undefined && faucet.amount !== null) {
parts.push(writeBool(true));
parts.push(writeF64(faucet.amount));
} else {
parts.push(writeBool(false));
}
return concatBytes(...parts);
}
function serializeAgentWalletCreation(agent: any): Uint8Array {
return concatBytes(
decodeAndValidateKey(agent.a), // agent: Pubkey (raw 32 bytes)
writeBool(agent.d) // delete: bool
);
}
function serializeUpdateUserSettings(settings: any): Uint8Array {
const parts: Uint8Array[] = [];
const leverageMap = settings.m || [];
parts.push(writeU64(leverageMap.length));
for (const [symbol, leverage] of leverageMap) {
parts.push(writeString(symbol));
parts.push(writeF64(leverage));
}
return concatBytes(...parts);
}
function serializeOracle(oracles: any[]): Uint8Array {
const parts: Uint8Array[] = [];
parts.push(writeU64(oracles.length));
for (const oracle of oracles) {
parts.push(writeU64(oracle.t)); // timestamp
parts.push(writeString(oracle.c)); // asset
parts.push(writeF64(oracle.px)); // price
}
return concatBytes(...parts);
}
// ============================================================================
// Main Transaction Serialization
// ============================================================================
/**
* Serialize transaction using wincode format for signing.
*
* Format: action_discriminant(u32) + action_data + nonce(u64) + account(32) + signer(32)
*/
export function serializeTransaction(
action: any,
account: string,
signer: string
): Uint8Array {
const actionType = action.type || '';
if (!(actionType in ACTION_CODES)) {
throw new Error(`Invalid action type: ${actionType}`);
}
const parts: Uint8Array[] = [];
// 1. Action discriminant (u32)
parts.push(writeU32(ACTION_CODES[actionType]));
// 2. Action-specific data
switch (actionType) {
case 'order':
parts.push(serializeOrders(action.orders || []));
break;
case 'oracle':
parts.push(serializeOracle(action.oracles || []));
break;
case 'faucet':
parts.push(serializeFaucet(action.faucet || {}));
break;
case 'updateUserSettings':
parts.push(serializeUpdateUserSettings(action.settings || {}));
break;
case 'agentWalletCreation':
parts.push(serializeAgentWalletCreation(action.agent || {}));
break;
}
// 3. Nonce (u64)
parts.push(writeU64(action.nonce));
// 4. Account (32 bytes)
parts.push(decodeAndValidateKey(account));
// 5. Signer (32 bytes)
parts.push(decodeAndValidateKey(signer));
return concatBytes(...parts);
}
/**
* Sign a transaction action for the exchange
*/
export function signTransaction(
secretKey: Uint8Array,
action: any,
account: string,
signer: string
): string {
// Serialize the transaction using wincode format
const message = serializeTransaction(action, account, signer);
// Sign with Ed25519
const signature = nacl.sign.detached(message, secretKey);
// Encode as base58
return bs58.encode(signature);
}