@renproject/rentx
Version:
XState Statemachines for tracking RenVM transactions reactively
303 lines (281 loc) • 10.7 kB
text/typescript
import RenJS from "@renproject/ren";
import { Actor, assign, MachineOptions, Receiver, Sender, spawn } from "xstate";
import { depositMachine, DepositMachineContext } from "../machines/deposit";
import { GatewayMachineContext } from "../machines/mint";
import { GatewaySession, GatewayTransaction } from "../types/transaction";
/*
Sample mintChainMap / lockChainMap implementations
We don't implement these to prevent mandating specific chains
const mintChainMap: {
[key in string]: (c: GatewayMachineContext) => MintChain<any>;
} = {
binanceSmartChain: (context: GatewayMachineContext) => {
const { destAddress, destNetwork } = context.tx;
const { providers } = context;
return new BinanceSmartChain(providers[destNetwork]).Account({
address: destAddress,
}) as MintChain<any>;
},
ethereum: (context: GatewayMachineContext): MintChain => {
const { destAddress, destNetwork } = context.tx;
const { providers } = context;
return Ethereum(providers[destNetwork]).Account({
address: destAddress,
}) as MintChain<any>;
},
};
const lockChainMap = {
bitcoin: () => Bitcoin(),
zcash: () => Zcash(),
bitcoinCash: () => BitcoinCash(),
};
*/
export const renLockAndMint = async (context: GatewayMachineContext) => {
const {
nonce,
destNetwork,
suggestedAmount,
sourceNetwork,
sourceAsset,
} = context.tx;
const { sdk, fromChainMap, toChainMap } = context;
const mint = await sdk.lockAndMint({
asset: sourceAsset.toUpperCase(),
suggestedAmount,
from: fromChainMap[sourceNetwork](context),
to: toChainMap[destNetwork](context),
nonce,
});
return mint;
};
// Format a transaction and get the gateway address
const txCreator = async (context: GatewayMachineContext) => {
// TX may be in a state where the gateway address was provided,
// but no deposit was picked up
if (!context.tx.nonce) {
context.tx.nonce = RenJS.utils.randomNonce().toString("hex");
}
const { targetAmount, sourceAsset } = context.tx;
try {
const fees = await context.sdk.getFees();
context.tx.suggestedAmount = Math.floor(
fees[sourceAsset.toLowerCase()].lock +
(Number(targetAmount) || 0.0001) * 1e8
);
} catch (error) {
console.error(error);
context.tx.suggestedAmount = 0.0008 * 1e8;
}
const minter = await renLockAndMint(context);
const gatewayAddress = minter?.gatewayAddress;
const newTx: GatewaySession = {
...context.tx,
gatewayAddress,
// Not serializable
// params: deposit.params,
};
return newTx;
};
// Listen for confirmations on the source chain
const depositListener = (
context: GatewayMachineContext | DepositMachineContext
) => (callback: Sender<any>, receive: Receiver<any>) => {
let cleanup = () => {};
renLockAndMint(context).then(async (minter) => {
cleanup = () => minter.removeAllListeners();
minter.on("deposit", async (deposit) => {
// Register event handlers prior to setup in case events land early
receive((event) => {
switch (event.type) {
case "SETTLE":
deposit
.confirmed()
.on("confirmation", (confs, targetConfs) => {
const confirmedTx = {
sourceTxConfs: confs,
sourceTxConfTarget: targetConfs,
};
callback({
type: "CONFIRMATION",
data: confirmedTx,
});
})
.then(() => {
callback({
type: "CONFIRMED",
});
});
break;
case "SIGN":
deposit
?.signed()
.on("status", (state) => console.log(state))
.then((v) =>
callback({
type: "SIGNED",
data: {
renResponse: v._queryTxResult?.out,
signature:
v._queryTxResult?.out?.signature,
},
})
)
.catch((e) =>
callback({ type: "SIGN_ERROR", data: e })
);
break;
case "MINT":
deposit
?.mint()
.on("transactionHash", (txHash) => {
const submittedTx = {
destTxHash: txHash,
};
callback({
type: "SUBMITTED",
data: submittedTx,
});
})
.catch((e) =>
callback({ type: "SUBMIT_ERROR", data: e })
);
break;
}
});
const txHash = await deposit.txHash();
const persistedTx = context.tx.transactions[txHash];
// Prevent deposit machine tx listeners from interacting with other deposits
const targetDeposit = (context as DepositMachineContext).deposit;
if (targetDeposit) {
if (targetDeposit.sourceTxHash !== txHash) {
console.error(
"wrong deposit:",
targetDeposit.sourceTxHash,
txHash
);
return;
}
}
// If we don't have a sourceTxHash, we haven't seen a deposit yet
const rawSourceTx: any = deposit.depositDetails.transaction;
const depositState: GatewayTransaction = persistedTx || {
sourceTxHash: txHash,
sourceTxAmount: rawSourceTx.amount,
sourceTxVOut: rawSourceTx.vOut,
rawSourceTx,
};
if (!persistedTx) {
callback({
type: "DEPOSIT",
data: { ...depositState },
});
} else {
callback("DETECTED");
}
});
receive((event) => {
switch (event.type) {
case "RESTORE":
try {
minter.processDeposit(event.data);
} catch (e) {
console.error(e);
}
break;
}
});
callback("LISTENING");
});
return () => {
cleanup();
};
};
// Spawn an actor that will listen for either all deposits to a gatewayAddress,
// or to a single deposit if present in the context
const listenerAction = assign<GatewayMachineContext>({
depositListenerRef: (
c: GatewayMachineContext | DepositMachineContext,
_e: any
) => {
let actorName = `${c.tx.id}SessionListener`;
const deposit = (c as DepositMachineContext).deposit;
if (deposit) {
actorName = `${deposit.sourceTxHash}DepositListener`;
}
if (c.depositListenerRef && !deposit) {
console.warn("listener already exists");
return c.depositListenerRef;
}
const cb = depositListener(c);
return spawn(cb, actorName);
},
});
const spawnDepositMachine = (
machineContext: DepositMachineContext,
name: string
) =>
spawn(
depositMachine
.withContext(machineContext as DepositMachineContext)
.withConfig({
actions: {
listenerAction: listenerAction as any,
},
}),
{
sync: true,
name,
}
) as Actor<any>;
export const mintConfig: Partial<MachineOptions<GatewayMachineContext, any>> = {
services: {
txCreator,
depositListener,
},
actions: {
spawnDepositMachine: assign({
depositMachines: (context, evt) => {
const machines = context.depositMachines || {};
if (machines[evt.data?.sourceTxHash] || !evt.data) {
return machines;
}
const machineContext = {
...context,
deposit: evt.data,
};
// We don't want child machines to have references to siblings
delete (machineContext as any).depositMachines;
machines[evt.data.sourceTxHash] = spawnDepositMachine(
machineContext,
`${evt.data.sourceTxHash}DepositMachine`
);
return machines;
},
}),
depositMachineSpawner: assign({
depositMachines: (context, _) => {
const machines = context.depositMachines || {};
for (let i of Object.entries(context.tx.transactions || {})) {
const machineContext = {
...context,
deposit: i[1],
};
// We don't want child machines to have references to siblings
delete (machineContext as any).depositMachines;
machines[i[0]] = spawnDepositMachine(
machineContext,
`${machineContext.deposit.sourceTxHash}DepositMachine`
);
}
return machines;
},
}),
listenerAction: listenerAction as any,
},
guards: {
isCompleted: ({ tx }, evt) =>
evt.data?.sourceTxAmount >= tx.targetAmount,
isExpired: ({ tx }) => tx.expiryTime < new Date().getTime(),
isCreated: ({ tx }) => (tx.gatewayAddress ? true : false),
},
};