Created
September 23, 2022 09:05
-
-
Save manimal1/2c2d44c6dc941cc307f08b40e6fbd6b5 to your computer and use it in GitHub Desktop.
Generated by XState Viz: https://xstate.js.org/viz
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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