Skip to content

Commit

Permalink
feat: allow safe json upload into osnap tx builder
Browse files Browse the repository at this point in the history
Signed-off-by: david <[email protected]>
  • Loading branch information
daywiss committed Mar 11, 2024
1 parent 77b4d8e commit e81a629
Show file tree
Hide file tree
Showing 7 changed files with 350 additions and 4 deletions.
5 changes: 5 additions & 0 deletions src/plugins/oSnap/components/Input/TransactionType.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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'
}
];
</script>
Expand Down
151 changes: 151 additions & 0 deletions src/plugins/oSnap/components/TransactionBuilder/SafeImport.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
<script setup lang="ts">
import { parseAmount } from '@/helpers/utils';
import { isAddress, FunctionFragment } from '@ethersproject/address';
import { isBigNumberish } from '@ethersproject/bignumber/lib/bignumber';
import { isHexString } from '@ethersproject/bytes';
import { SafeImportTransaction, GnosisSafe } from '../../types';
import { createSafeImportTransaction, parseValueInput, isSafeFile, getABIWriteFunctions } from '../../utils';
import AddressInput from '../Input/Address.vue';
const props = defineProps<{
transaction: SafeImportTransaction | undefined;
setTransactionAsInvalid(): void;
}>();
const emit = defineEmits<{
updateTransaction: [transaction: SafeImportTransaction];
}>();
const file = ref(null);
const safeTransactions:null | GnosisSafe.BatchFile = ref(null);
const selectedTransactionIndex: null | number = ref(null);
const importFile = async (file: File): Promise<BatchFile> => {
return new Promise((res,rej) => {
const reader = new FileReader()
reader.readAsText(file)
reader.onload = async () => {
try{
const json = JSON.parse(
reader.result,
)
if(isSafeFile(json)){
return res(json)
}
throw new Error('Not a Gnosis Safe transaction file!')
}catch(err){
rej(err)
}
}
})
}
const handleFileUpload = (event) => {
file.value = event.target.files[0];
if(!file.value) return;
importFile(file.value).then(result=>{
safeTransactions.value = result
console.log(safeTransactions.value)
}).catch(console.error)
};
function updateTransaction(partialTx:PartialSafeImportTransaction) {
try {
// if (!isToValid.value) {
// throw new Error('"To" address invalid');
// }
// if (!isValueValid.value) {
// throw new Error('"Value" amount invalid invalid');
// }
// if (!isDataValid.value) {
// throw new Error('"Data" field invalid');
// }
const tx = createSafeImportTransaction(partialTx)
console.log(tx)
emit('updateTransaction', tx);
} catch (error) {
console.error(error);
props.setTransactionAsInvalid();
}
}
// need a way to select which transaction we want toimport
watch(selectedTransactionIndex, (index=>{
console.log({index},safeTransactions.value)
if(index >= 0){
const convertedTxs = convertSafeTransactions(safeTransactions.value)
console.log(convertedTxs[index])
updateTransaction(convertedTxs[index])
}
}));
type PartialSafeImportTransaction = Omit<SafeImportTransaction | 'formatted'>
function convertSafeTransactions(txs?:GnosisSafe.BatchFile):PartialSafeImportTransaction[] {
if(!txs) return []
return txs.transactions.map(safeTx=>{
return {
methodName:safeTx.contractMethod?.name,
parameters:safeTx.contractMethod?.inputs?.map(({name,type})=>({
name,
type,
value:safeTx.contractInputsValues?.[name]
})),
to:safeTx.to,
value: safeTx.value,
data: safeTx.data,
}
})
}
const objectFromEntriesSorted = (obj: Object) => {
// set as array first to the correct preserve order
let entries = Object.entries(obj);
let sorted: Array<[string, any]> = [];
keyOrder.forEach(key => {
if (obj.hasOwnProperty(key)) {
sorted.push([key, obj[key]]);
entries = entries.filter(item => item[0] !== key);
}
});
// ensure we don't filter out any items we didn't explicitly sort
return [...sorted, ...entries];
};
</script>

<template>
<div class="space-y-2">
<input type="file" @change="handleFileUpload($event)" />
<div v-if="file">
Selected file: {{ file.name }}
</div>
</div>
<div
v-if="safeTransactions?.transactions.length > 0"
class="flex w-full flex-col gap-4 rounded-2xl border border-skin-border p-3 md:p-4 relative"
>
<UiSelect v-model="selectedTransactionIndex">
<template #label>Select Transaction</template>
<option v-for="(tx,i) in safeTransactions?.transactions" :key="i" :value="i">
{{i+1}}. {{ tx.contractMethod.name }}
</option>
</UiSelect>
</div>
<div
v-if="props?.transaction?.parameters.length"
class="flex w-full flex-col gap-4 rounded-2xl border border-skin-border p-3 md:p-4 relative"
>
<ReadOnly v-for="param in props.transaction.parameters">
<strong
class="mr-2 inline-block whitespace-nowrap first-letter:capitalize"
>{{ param.name }} ({{param.type}})</strong
>
<span class="break-all">{{ param.value }}</span>
</ReadOnly>
</div>
</template>
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -109,5 +110,12 @@ function setTransactionAsInvalid() {
:setTransactionAsInvalid="setTransactionAsInvalid"
@update-transaction="updateTransaction"
/>

<SafeImport
v-if="transaction.type === 'safeImport'"
:transaction="newTransaction as TRawTransaction"
:setTransactionAsInvalid="setTransactionAsInvalid"
@update-transaction="updateTransaction"
/>
</div>
</template>
3 changes: 2 additions & 1 deletion src/plugins/oSnap/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1519,7 +1519,8 @@ export const transactionTypes = [
'transferFunds',
'transferNFT',
'contractInteraction',
'raw'
'raw',
'safeImport'
] as const;

export const solidityZeroHexString =
Expand Down
87 changes: 86 additions & 1 deletion src/plugins/oSnap/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,8 @@ export type Transaction =
| RawTransaction
| ContractInteractionTransaction
| TransferNftTransaction
| TransferFundsTransaction;
| TransferFundsTransaction
| SafeImportTransaction;

/**
* Represents the fields that all transactions share.
Expand All @@ -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.
Expand Down Expand Up @@ -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<string, string>
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[]
}
}
22 changes: 21 additions & 1 deletion src/plugins/oSnap/utils/transactions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import {
RawTransaction,
Token,
TransferFundsTransaction,
TransferNftTransaction
TransferNftTransaction,
SafeImportTransaction,
} from '../types';
import { encodeMethodAndParams } from './abi';

Expand Down Expand Up @@ -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
}
}
Loading

0 comments on commit e81a629

Please sign in to comment.