From a49f928d30fe4e506c0040f5505627a2fd6846e3 Mon Sep 17 00:00:00 2001 From: david Date: Mon, 11 Mar 2024 13:32:03 -0400 Subject: [PATCH 01/27] feat: allow safe json upload into osnap tx builder Signed-off-by: david --- .../components/Input/TransactionType.vue | 5 + .../TransactionBuilder/SafeImport.vue | 151 ++++++++++++++++++ .../TransactionBuilder/Transaction.vue | 8 + src/plugins/oSnap/constants.ts | 3 +- src/plugins/oSnap/types.ts | 87 +++++++++- src/plugins/oSnap/utils/transactions.ts | 22 ++- src/plugins/oSnap/utils/validators.ts | 78 ++++++++- 7 files changed, 350 insertions(+), 4 deletions(-) create mode 100644 src/plugins/oSnap/components/TransactionBuilder/SafeImport.vue diff --git a/src/plugins/oSnap/components/Input/TransactionType.vue b/src/plugins/oSnap/components/Input/TransactionType.vue index 7cadab8c0..53c585996 100644 --- a/src/plugins/oSnap/components/Input/TransactionType.vue +++ b/src/plugins/oSnap/components/Input/TransactionType.vue @@ -36,6 +36,11 @@ const transactionTypesWithDetails: { type: 'raw', title: 'Raw transaction', description: 'Send a raw transaction' + }, + { + type: 'safeImport', + title: 'Import Safe File', + description: 'Import JSON file exported from Gnosis Safe transaction builder' } ]; diff --git a/src/plugins/oSnap/components/TransactionBuilder/SafeImport.vue b/src/plugins/oSnap/components/TransactionBuilder/SafeImport.vue new file mode 100644 index 000000000..02596d20b --- /dev/null +++ b/src/plugins/oSnap/components/TransactionBuilder/SafeImport.vue @@ -0,0 +1,151 @@ + + + diff --git a/src/plugins/oSnap/components/TransactionBuilder/Transaction.vue b/src/plugins/oSnap/components/TransactionBuilder/Transaction.vue index e5251c025..7011fd88f 100644 --- a/src/plugins/oSnap/components/TransactionBuilder/Transaction.vue +++ b/src/plugins/oSnap/components/TransactionBuilder/Transaction.vue @@ -16,6 +16,7 @@ import ContractInteraction from './ContractInteraction.vue'; import RawTransaction from './RawTransaction.vue'; import TransferFunds from './TransferFunds.vue'; import TransferNFT from './TransferNFT.vue'; +import SafeImport from './SafeImport.vue'; const props = defineProps<{ transaction: TTransaction; @@ -109,5 +110,12 @@ function setTransactionAsInvalid() { :setTransactionAsInvalid="setTransactionAsInvalid" @update-transaction="updateTransaction" /> + + diff --git a/src/plugins/oSnap/constants.ts b/src/plugins/oSnap/constants.ts index 4c1797faf..ee03a6ee9 100644 --- a/src/plugins/oSnap/constants.ts +++ b/src/plugins/oSnap/constants.ts @@ -1519,7 +1519,8 @@ export const transactionTypes = [ 'transferFunds', 'transferNFT', 'contractInteraction', - 'raw' + 'raw', + 'safeImport' ] as const; export const solidityZeroHexString = diff --git a/src/plugins/oSnap/types.ts b/src/plugins/oSnap/types.ts index 998aaeef7..98118e61a 100644 --- a/src/plugins/oSnap/types.ts +++ b/src/plugins/oSnap/types.ts @@ -77,7 +77,8 @@ export type Transaction = | RawTransaction | ContractInteractionTransaction | TransferNftTransaction - | TransferFundsTransaction; + | TransferFundsTransaction + | SafeImportTransaction; /** * Represents the fields that all transactions share. @@ -93,6 +94,24 @@ export type BaseTransaction = { formatted: OptimisticGovernorTransaction; isValid?: boolean; }; +/** + * Represents a transaction that interacts with an arbitrary contract from safe json file import. + * + * @field `abi` field is the ABI of the contract that the transaction interacts with, represented as a JSON string. + * + * @field `methodName` field is the name of the method on the contract that the transaction calls. + * + * @field `parameters` field is an array of strings that represent the parameters that the method takes. NOTE: some methods take arrays or tuples as arguments, so some of these strings in the array may be JSON formatted arrays or tuples. + */ +export type SafeImportTransaction = BaseTransaction & { + type: 'safeImport'; + methodName?: string; + parameters?: Array<{ + name: string; + type: string; + value: string | boolean | number | undefined | null; + }> +}; /** * Represents a 'raw' transaction that does not have any additional fields. @@ -448,3 +467,69 @@ export type SpaceConfigResponse = bondToken: boolean; bondAmount: boolean; }; + +export namespace GnosisSafe { + export interface ProposedTransaction { + id: number + contractInterface: ContractInterface | null + description: { + to: string + value: string + customTransactionData?: string + contractMethod?: ContractMethod + contractFieldsValues?: Record + contractMethodIndex?: string + nativeCurrencySymbol?: string + networkPrefix?: string + } + raw: { to: string; value: string; data: string } + } + + export interface ContractInterface { + methods: ContractMethod[] + } + + export interface Batch { + id: number | string + name: string + transactions: ProposedTransaction[] + } + + export interface BatchFile { + version: string + chainId: string + createdAt: number + meta: BatchFileMeta + transactions: BatchTransaction[] + } + + export interface BatchFileMeta { + txBuilderVersion?: string + checksum?: string + createdFromSafeAddress?: string + createdFromOwnerAddress?: string + name: string + description?: string + } + + export interface BatchTransaction { + to: string + value: string + data?: string + contractMethod?: ContractMethod + contractInputsValues?: { [key: string]: string } + } + + export interface ContractMethod { + inputs: ContractInput[] + name: string + payable: boolean + } + + export interface ContractInput { + internalType: string + name: string + type: string + components?: ContractInput[] + } +} diff --git a/src/plugins/oSnap/utils/transactions.ts b/src/plugins/oSnap/utils/transactions.ts index e8fbf5eb2..c27c22c5e 100644 --- a/src/plugins/oSnap/utils/transactions.ts +++ b/src/plugins/oSnap/utils/transactions.ts @@ -7,7 +7,8 @@ import { RawTransaction, Token, TransferFundsTransaction, - TransferNftTransaction + TransferNftTransaction, + SafeImportTransaction, } from '../types'; import { encodeMethodAndParams } from './abi'; @@ -166,3 +167,22 @@ export function parseValueInput(input: string) { } return parseAmount(input); } + +export function createSafeImportTransaction(params:{ + to: string; + value: string; + data: string, + methodName:string, + parameters: string[]; +}):SafeImportTransaction { + const formatted = createFormattedOptimisticGovernorTransaction({ + to:params.to, + value:params.value, + data:params.data, + }); + return { + type:'safeImport', + ...params, + formatted + } +} diff --git a/src/plugins/oSnap/utils/validators.ts b/src/plugins/oSnap/utils/validators.ts index 4a69a3836..2ef7e3afb 100644 --- a/src/plugins/oSnap/utils/validators.ts +++ b/src/plugins/oSnap/utils/validators.ts @@ -9,7 +9,7 @@ import { isBigNumberish } from '@ethersproject/bignumber/lib/bignumber'; import { isHexString } from '@ethersproject/bytes'; import getProvider from '@snapshot-labs/snapshot.js/src/utils/provider'; import { OPTIMISTIC_GOVERNOR_ABI } from '../constants'; -import { BaseTransaction, NFT, Token, Transaction } from '../types'; +import { BaseTransaction, NFT, Token, Transaction, GnosisSafe } from '../types'; import { parseUnits } from '@ethersproject/units'; import { useMemoize } from '@vueuse/core'; @@ -136,3 +136,79 @@ export const checkIsContract = useMemoize( async (address: string, network: string) => await isContractAddress(address, network) ); + +// check if json is a safe json type +export const isSafeFile = (input: any): input is GnosisSafe.BatchFile => { + const $io0 = (input: any): boolean => + "string" === typeof input.version && + "string" === typeof input.chainId && + "number" === typeof input.createdAt && + "object" === typeof input.meta && + null !== input.meta && + $io1(input.meta) && + Array.isArray(input.transactions) && + input.transactions.every( + (elem: any) => "object" === typeof elem && null !== elem && $io2(elem), + ); + const $io1 = (input: any): boolean => + (null === input.txBuilderVersion || + undefined === input.txBuilderVersion || + "string" === typeof input.txBuilderVersion) && + (null === input.checksum || + undefined === input.checksum || + "string" === typeof input.checksum) && + (null === input.createdFromSafeAddress || + undefined === input.createdFromSafeAddress || + "string" === typeof input.createdFromSafeAddress) && + (null === input.createdFromOwnerAddress || + undefined === input.createdFromOwnerAddress || + "string" === typeof input.createdFromOwnerAddress) && + "string" === typeof input.name && + (null === input.description || + undefined === input.description || + "string" === typeof input.description); + const $io2 = (input: any): boolean => + "string" === typeof input.to && + "string" === typeof input.value && + (null === input.data || + undefined === input.data || + "string" === typeof input.data) && + (null === input.contractMethod || + undefined === input.contractMethod || + ("object" === typeof input.contractMethod && + null !== input.contractMethod && + $io3(input.contractMethod))) && + (null === input.contractInputsValues || + undefined === input.contractInputsValues || + ("object" === typeof input.contractInputsValues && + null !== input.contractInputsValues && + false === Array.isArray(input.contractInputsValues) && + $io5(input.contractInputsValues))); + const $io3 = (input: any): boolean => + Array.isArray(input.inputs) && + input.inputs.every( + (elem: any) => "object" === typeof elem && null !== elem && $io4(elem), + ) && + "string" === typeof input.name && + "boolean" === typeof input.payable; + const $io4 = (input: any): boolean => + (undefined === input.internalType || + "string" === typeof input.internalType) && + "string" === typeof input.name && + "string" === typeof input.type && + (null === input.components || + undefined === input.components || + (Array.isArray(input.components) && + input.components.every( + (elem: any) => + "object" === typeof elem && null !== elem && $io4(elem), + ))); + const $io5 = (input: any): boolean => + Object.keys(input).every((key: any) => { + const value = input[key]; + if (undefined === value) return true; + return "string" === typeof value; + }); + return "object" === typeof input && null !== input && $io0(input); +}; + From 6217d746c5aaf242cdba0a560a40ed7f30f4038f Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Mon, 18 Mar 2024 15:51:33 +0200 Subject: [PATCH 02/27] parse batchfile & populate input fields with values --- .../components/Input/MethodParameter.vue | 18 +- .../TransactionBuilder/SafeImport.vue | 220 ++++++++++-------- src/plugins/oSnap/types.ts | 92 ++++---- src/plugins/oSnap/utils/abi.ts | 58 ++++- src/plugins/oSnap/utils/safeImport.ts | 33 +++ src/plugins/oSnap/utils/transactions.ts | 175 ++++++++++++-- src/plugins/oSnap/utils/validators.ts | 54 +++-- 7 files changed, 459 insertions(+), 191 deletions(-) create mode 100644 src/plugins/oSnap/utils/safeImport.ts diff --git a/src/plugins/oSnap/components/Input/MethodParameter.vue b/src/plugins/oSnap/components/Input/MethodParameter.vue index a0f9025c4..33c82b943 100644 --- a/src/plugins/oSnap/components/Input/MethodParameter.vue +++ b/src/plugins/oSnap/components/Input/MethodParameter.vue @@ -8,6 +8,7 @@ import { hexZeroPad, isBytesLike } from '@ethersproject/bytes'; const props = defineProps<{ parameter: ParamType; value: string; + validateOnMount?: boolean; }>(); const emit = defineEmits<{ @@ -37,8 +38,12 @@ const inputType = computed(() => { const label = `${props.parameter.name} (${props.parameter.type})`; const arrayPlaceholder = `E.g. ["text", 123, 0x123]`; +const newValue = ref(props.value); + +const validationState = ref(true); +const isInputValid = computed(() => validationState.value); -const isInputValid = computed(() => { +function validate() { if (!isDirty.value) return true; if (isAddressInput.value) return isAddress(newValue.value); if (isArrayInput.value) return validateArrayInput(newValue.value); @@ -46,9 +51,7 @@ const isInputValid = computed(() => { if (isBytes32Input.value) return validateBytes32Input(newValue.value); if (isBytesInput.value) return validateBytesInput(newValue.value); return true; -}); - -const newValue = ref(props.value); +} watch(props.parameter, () => { newValue.value = ''; @@ -56,6 +59,7 @@ watch(props.parameter, () => { }); watch(newValue, () => { + validationState.value = validate(); emit('updateParameterValue', newValue.value); }); @@ -103,6 +107,12 @@ function formatBytes32() { newValue.value = hexZeroPad(newValue.value, 32); } } +onMounted(() => { + if (props.validateOnMount) { + isDirty.value = true; + } + validationState.value = validate(); +});