Skip to content

Instantly share code, notes, and snippets.

@manimal1
Created September 23, 2022 09:05
Show Gist options
  • Save manimal1/2c2d44c6dc941cc307f08b40e6fbd6b5 to your computer and use it in GitHub Desktop.
Save manimal1/2c2d44c6dc941cc307f08b40e6fbd6b5 to your computer and use it in GitHub Desktop.
Generated by XState Viz: https://xstate.js.org/viz
// Available variables:
// - Machine
// - interpret
// - assign
// - send
// - sendParent
// - spawn
// - raise
// - actions
// - XState (all XState exports)
const machineDeclaration = {
id: 'transactionModal',
initial: 'unknown',
on: {
AUTH: {
actions: 'setSigner',
target: 'unknown',
},
CANCEL: 'cancelled',
},
states: {
building: {
invoke: {
onDone: {
actions: 'handleBuildTransactionDone',
target: 'estimating',
},
onError: {
actions: 'handleError',
target: '#transactionModal.error',
},
src: 'buildTransaction',
},
},
cancelled: {
entry: 'onCancelled',
on: {
RESET: {
target: 'idle',
},
START: {
actions: ['handleReset', 'handleStart'],
target: 'building',
},
START_TX_DATA: {
actions: ['handleReset', 'handleStartTxData'],
target: 'building',
},
},
},
error: {
entry: 'onError',
exit: 'handleExitError',
on: {
RETRY: {
actions: 'incrementRetryCounter',
target: 'building',
},
},
},
estimated: {
on: {
SUBMIT: {
actions: 'handleSubmit',
target: 'submitted',
},
},
},
estimating: {
initial: 'pending',
onDone: '#transactionModal.estimated',
states: {
done: {
type: 'final',
},
pending: {
invoke: {
onDone: {
actions: 'handleEstimateTransactionDone',
target: 'done',
},
onError: {
actions: 'handleError',
target: '#transactionModal.error',
},
src: 'estimateTransaction',
},
},
},
},
idle: {
entry: 'handleReset',
on: {
START: {
actions: 'handleStart',
target: 'building',
},
START_TX_DATA: {
actions: 'handleStartTxData',
target: 'building',
},
},
},
sent: {
entry: 'onSent',
on: {
RESET: {
target: 'idle',
},
START: {
actions: ['handleReset', 'handleStart'],
target: 'building',
},
START_TX_DATA: {
actions: ['handleReset', 'handleStartTxData'],
target: 'building',
},
},
},
submitted: {
initial: 'validating',
on: {
ABORT: {
target: '#transactionModal.estimated',
},
CANCEL: undefined,
},
states: {
delayed: {
after: {
FAT_FINGER_DELAY: 'sending',
},
},
sending: {
invoke: {
onDone: {
actions: 'handleSendingDone',
target: '#transactionModal.sent',
},
onError: {
actions: 'handleError',
target: '#transactionModal.error',
},
src: 'sendTransaction',
},
on: {
// Once we are in the `sending` state, there is no turning back.
ABORT: undefined,
},
},
validating: {
invoke: {
onDone: 'delayed',
onError: {
actions: 'handleError',
target: '#transactionModal.error',
},
src: 'validateTransaction',
},
on: {
ABORT: {
target: '#transactionModal.estimated',
},
CANCEL: {
target: '#transactionModal.idle',
},
},
},
},
},
unknown: {
always: [
{
cond: 'hasSignerAndFunction',
target: 'building',
},
{
target: 'idle',
},
],
},
},
};
const machineOptions = {
actions: {
incrementRetryCounter: ({
retryCounter: (context) => (context.retryCounter ?? 0) + 1,
}),
handleBuildTransactionDone: ({
txData: (_, event) => event.data,
}),
handleError: ({
error: (_, event) => ({ ...extractError(event.data), details: event.data }),
}),
handleEstimateTransactionDone: ({
gasLimit: (_, event) => event.data,
}),
handleExitError: ({
error: undefined,
}),
handleReset: ({
retryCounter: undefined,
contractTransaction: undefined,
error: undefined,
gasLimit: undefined,
gasRelayerData: undefined,
originAddress: undefined,
paymasterAddress: undefined,
sendFunction: undefined,
submittedSignerAddress: undefined,
txData: undefined,
txDataFn: undefined,
useGasRelayer: undefined,
}),
handleSendingDone: ({
contractTransaction: (_, event) => event.data,
}),
handleStart: ({
sendFunction: (_, event) => event.sendFunction,
submittedSignerAddress: (_, event) => event.submittedSignerAddress,
vaultProxy: (_, event) => event.vaultProxy,
}),
handleStartTxData: ({
submittedSignerAddress: (_, event) => event.submittedSignerAddress,
txDataFn:
(_, { txDataFn, submittedSignerAddress }) =>
(originAddress) =>
txDataFn(originAddress).then((txData) => ({
...txData,
from: submittedSignerAddress,
value: txData.value && BigNumber.from(txData.value),
})),
vaultProxy: (_, event) => event.vaultProxy,
}),
handleSubmit: ({
gasRelayerData: (_, event) => event.gasRelayerData,
gasRelayerEndpoint: (_, event) => event.gasRelayerEndpoint,
originAddress: (_, event) => event.originAddress,
paymasterAddress: (_, event) => event.paymasterAddress,
topUpGasRelayer: (_, event) => event.topUpGasRelayer,
useGasRelayer: (_, event) => event.useGasRelayer,
}),
setSigner: ({
signer: (_, event) => event.signer,
signerAddress: (_, event) => event.signerAddress,
}),
},
delays: {
// Grant users a short time period in which they can still abort before
// the transaction is finally sent.
FAT_FINGER_DELAY: 2000,
},
guards: {
hasSignerAndFunction: (context) => {
return !!(context.sendFunction && context.signer);
},
},
services: {
buildTransaction: async (context) => {
if (context.sendFunction) {
const value =
context.sendFunction.options.value === undefined
? undefined
: BigNumber.from(context.sendFunction.options.value);
return {
...(await context.sendFunction.populate()),
from: context.submittedSignerAddress,
value,
};
}
if (!context.txDataFn) {
throw new Error('Missing transaction function');
}
return context.txDataFn();
},
estimateTransaction: async (context) => {
const { signerAddress, txData, provider, submittedSignerAddress, signer } = context;
if (!(signerAddress && signer)) {
throw new Error('Wallet not connected.');
}
if (!txData) {
throw new Error('Missing transaction data');
}
if (!safeSameAddress(signerAddress, submittedSignerAddress)) {
throw new Error(
`Invalid wallet selected. Did you switch account? You are connected with ${signerAddress} but the transaction was created with ${submittedSignerAddress}`,
);
}
try {
const gas = await provider.estimateGas(txData);
return gas.mul(115).div(100);
} catch (error) {
await captureTransactionError(error, context);
throw error;
}
},
sendTransaction: async (context) => {
const {
gasLimit,
gasRelayerData,
gasRelayerEndpoint,
paymasterAddress,
originAddress,
provider,
signer,
signerAddress,
submittedSignerAddress,
topUpGasRelayer,
txData: contextTxData,
txDataFn,
useGasRelayer,
} = context;
if (!(signerAddress && signer)) {
throw new Error('Wallet not connected.');
}
if (!safeSameAddress(signerAddress, submittedSignerAddress)) {
throw new Error(
`Invalid wallet selected. Did you switch account? You are connected with ${signerAddress} but the transaction was created with ${submittedSignerAddress}`,
);
}
// If txDataFn is defined, rebuild txData with originAddress
const txData = txDataFn ? await txDataFn(originAddress) : contextTxData;
if (!txData) {
throw new Error('Missing transaction data');
}
if (useGasRelayer) {
if (!(paymasterAddress && gasRelayerData && gasRelayerEndpoint)) {
throw new Error('Missing Gas Relayer Data');
}
const { relayWorkerAddress, relayHubAddress, maxAcceptanceBudget, minGasPrice, ready } = gasRelayerData;
if (!ready) {
throw new Error('Gas relayer not ready. Please try submitting the transaction again.');
}
if (!gasLimit) {
throw new Error('Missing gas limit');
}
const { gasRelayerPayload, gasRelayerTransaction } = await createSignedGasRelayerTransaction({
gasLimit,
gasPrice: minGasPrice.mul(110).div(100),
maxAcceptanceBudget,
paymasterData: utils.defaultAbiCoder.encode(['bool'], [!!topUpGasRelayer]),
pctRelayFee: 10,
relayHub: relayHubAddress,
relayWorker: relayWorkerAddress,
signer,
txData,
vaultPaymaster: paymasterAddress,
});
try {
// Make sure that the MetaTx doesn't revert
// TODO: Ideally we would also monitor the return value and ensure that there has not been an error in an inside call.
// For that however, we would need an endpoint that runs this fn: https://github.com/opengsn/gsn/blob/12e73fc179af8fef2c6364c9ef8f613b9fefbea2/packages/relay/src/RelayServer.ts#L264 as it provides one of the values that we need for the relayCall
await provider.call(gasRelayerTransaction);
} catch (error) {
throw new Error(`Invalid transaction: ${error}`);
}
const endpoint = `${gasRelayerEndpoint}/gsn1/relay`;
const response = await (
await fetch(endpoint, {
body: gasRelayerPayload,
headers: {
'Content-Type': 'application/json',
},
method: 'POST',
})
).json();
const signedTx = response?.signedTx;
if (typeof signedTx === 'undefined') {
if (typeof response?.error === 'string') {
throw new Error(`An error occurred: ${response.error}`);
}
throw new Error('An error occurred in the relayer. Please try again.');
}
const parsedTx = utils.parseTransaction(response.signedTx);
return { ...parsedTx, from: submittedSignerAddress };
}
const transaction = await signer.sendTransaction({ ...txData, gasLimit });
return transaction;
},
validateTransaction: async (context) => {
const { gasLimit: limit, provider, signer, signerAddress, submittedSignerAddress, txData } = context;
if (!(signerAddress && signer)) {
throw new Error('Wallet not connected.');
}
if (!safeSameAddress(signerAddress, submittedSignerAddress)) {
throw new Error(
`Invalid wallet selected. Did you switch account? You are connected with ${signerAddress} but the transaction was created with ${submittedSignerAddress}.`,
);
}
if (!txData) {
throw new Error('Missing transaction data.');
}
try {
await provider.call({ ...txData, gasLimit: limit });
} catch (error) {
await captureTransactionError(error, context);
throw error;
}
},
},
};
const transactionMachine = Machine(machineDeclaration, machineOptions);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment