diff --git a/snapshot-spaces b/snapshot-spaces index c5746dcac69..2f48598cb69 160000 --- a/snapshot-spaces +++ b/snapshot-spaces @@ -1 +1 @@ -Subproject commit c5746dcac69257ede837406ab00f3001ca4c6912 +Subproject commit 2f48598cb69164609bd0c02a65cddd223dc74af2 diff --git a/src/components/BaseModalSelectItem.vue b/src/components/BaseModalSelectItem.vue index 7f58096fb6f..778487c1809 100644 --- a/src/components/BaseModalSelectItem.vue +++ b/src/components/BaseModalSelectItem.vue @@ -23,7 +23,7 @@ defineProps<{ /> {{ tag }} - + diff --git a/src/components/ModalOsnap.vue b/src/components/ModalOsnap.vue new file mode 100644 index 00000000000..a84c005dfeb --- /dev/null +++ b/src/components/ModalOsnap.vue @@ -0,0 +1,62 @@ + + + diff --git a/src/components/SettingsPluginsBlock.vue b/src/components/SettingsPluginsBlock.vue index 6daa52915f4..49ebe7bd997 100644 --- a/src/components/SettingsPluginsBlock.vue +++ b/src/components/SettingsPluginsBlock.vue @@ -14,6 +14,9 @@ const modalPluginsOpen = ref(false); function handleEditPlugins(name: string) { if (props.isViewOnly) return; + // the oSnap plugin does not require any configuration + // so we don't need to open the modal + if (name === 'oSnap') return; currentPlugin.value = {}; currentPlugin.value[name] = clone(form.value.plugins[name]); modalPluginsOpen.value = true; diff --git a/src/components/SettingsTreasuriesBlock.vue b/src/components/SettingsTreasuriesBlock.vue index b7820456b26..e7440f94773 100644 --- a/src/components/SettingsTreasuriesBlock.vue +++ b/src/components/SettingsTreasuriesBlock.vue @@ -1,9 +1,10 @@ diff --git a/src/components/SettingsTreasuriesBlockItem.vue b/src/components/SettingsTreasuriesBlockItem.vue index 30e09d7ccd4..a82b99d907c 100644 --- a/src/components/SettingsTreasuriesBlockItem.vue +++ b/src/components/SettingsTreasuriesBlockItem.vue @@ -4,32 +4,26 @@ import { TreasuryWallet } from '@/helpers/interfaces'; defineProps<{ treasuries: TreasuryWallet[]; isViewOnly?: boolean; + hasOsnapPlugin: boolean; }>(); -const emit = defineEmits(['removeTreasury', 'editTreasury']); +const emit = defineEmits<{ + removeTreasury: [index: number]; + editTreasury: [index: number]; + configureOsnap: [index: number, isEnabled: boolean]; +}>(); diff --git a/src/components/SettingsTreasuriesBlockItemButton.vue b/src/components/SettingsTreasuriesBlockItemButton.vue new file mode 100644 index 00000000000..3feb8f35d2c --- /dev/null +++ b/src/components/SettingsTreasuriesBlockItemButton.vue @@ -0,0 +1,65 @@ + + + diff --git a/src/components/SettingsTreasuryActivateOsnapButton.vue b/src/components/SettingsTreasuryActivateOsnapButton.vue new file mode 100644 index 00000000000..81eb66f90e4 --- /dev/null +++ b/src/components/SettingsTreasuryActivateOsnapButton.vue @@ -0,0 +1,21 @@ + + + diff --git a/src/components/SpaceCreateLegacyOsnap.vue b/src/components/SpaceCreateLegacyOsnap.vue new file mode 100644 index 00000000000..056260f257e --- /dev/null +++ b/src/components/SpaceCreateLegacyOsnap.vue @@ -0,0 +1,50 @@ + + + diff --git a/src/components/SpaceCreateOsnap.vue b/src/components/SpaceCreateOsnap.vue new file mode 100644 index 00000000000..12dd6edd1f8 --- /dev/null +++ b/src/components/SpaceCreateOsnap.vue @@ -0,0 +1,39 @@ + + + diff --git a/src/components/SpaceCreateVoting.vue b/src/components/SpaceCreateVoting.vue index 0a999e25ac4..1c945e2e3a3 100644 --- a/src/components/SpaceCreateVoting.vue +++ b/src/components/SpaceCreateVoting.vue @@ -1,12 +1,16 @@ diff --git a/src/composables/useTxStatus.ts b/src/composables/useTxStatus.ts index 687a66d6cef..ed70db88333 100644 --- a/src/composables/useTxStatus.ts +++ b/src/composables/useTxStatus.ts @@ -15,14 +15,14 @@ export function useTxStatus() { return pendingTransactions.value.filter(tx => tx.hash); }); - const createPendingTransaction = () => { + const createPendingTransaction = (hash?: string) => { const createdAt = Date.now(); const id = createdAt.toString(); const tx = { id, network: web3.value.network.key, createdAt, - hash: null + hash: hash ?? null }; pendingTransactions.value.push(tx); return id; diff --git a/src/plugins/oSnap/Create.vue b/src/plugins/oSnap/Create.vue new file mode 100644 index 00000000000..e040cd1f528 --- /dev/null +++ b/src/plugins/oSnap/Create.vue @@ -0,0 +1,300 @@ + + + diff --git a/src/plugins/oSnap/Proposal.vue b/src/plugins/oSnap/Proposal.vue new file mode 100644 index 00000000000..7af5b1561f4 --- /dev/null +++ b/src/plugins/oSnap/Proposal.vue @@ -0,0 +1,134 @@ + + + diff --git a/src/plugins/oSnap/README.md b/src/plugins/oSnap/README.md new file mode 100644 index 00000000000..d9065ad4246 --- /dev/null +++ b/src/plugins/oSnap/README.md @@ -0,0 +1,17 @@ +# oSnap Snapshot Plugin + +This is a Snapshot plugin that facilitates using the Optimistic Governor to execute a set of transactions. + +See https://docs.snapshot.org/user-guides/plugins for general info about Snapshot plugin development. + +## Terms + +There are some terms that can be confusing in this plugin, because they are used to mean different things in different contexts. + +* Proposal — in the context of a normal Snapshot vote, regardless of plugins, "Proposal" refers to the set of questions that gets submitted to Snapshot and presented to voters. In the context of the Optimistic Governor, "Proposal" refers to the set of transactions that are submitted to the Optimistic Governor contract to be executed. This can be confusing because a Snapshot "Proposal" that uses the oSnap plugin will itself have an Optimistic Governor "Proposal" for the transactions that it aims to execute. As far as possible we have prefixed Optimistic Governor proposals with "OG" in the code to avoid confusion. + +* Assertion — Optimistic Oracle V3 calls a piece of information that is asserted as true an "assertion". In the context of the Optimistic Governor, an assertion is made on the Optimistic Oracle which states that the specified set of transactions is valid and should be executed. Some key information about the Optimistic Governor proposal can only be found in the context of assertions, such as the assertion transaction hash and log index which are used to generate links to the Optimistic Oracle UI. + +* SafeSnap and oSnap — oSnap was originally part of the SafeSnap plugin, hence the similar names. SafeSnap uses the Reality oracle, while oSnap provides the option to use the Optimistic Oracle instead. Eventually it was decided that oSnap deserves to be its own plugin, and so it was split off from SafeSnap. However, for legacy support reasons, the oSnap functionality in the SafeSnap plugin is still available. + +* Votes — both Snapshot and the Optimistic Oracle use votes for their function. The _Snapshot_ vote takes place first in the context of oSnap. When creating a Snapshot Proposal with oSnap, the Snapshot vote takes place first. Only if the Snapshot vote passes can transactions be proposed to the Optimistic Governor. In fact, if the assertion for an Optimistic Governor proposal is disputed, the Optimistic Governor proposal is immediately deleted by the contract. This means that the Optimistic Oracle vote that proceeds from the dispute has no bearing on the Optimistic Governor proposal. The Optimistic Oracle vote is only used to determine whether the disputer or the proposer loses their bond. \ No newline at end of file diff --git a/src/plugins/oSnap/components/ExternalLink.vue b/src/plugins/oSnap/components/ExternalLink.vue new file mode 100644 index 00000000000..b8dc65c96a2 --- /dev/null +++ b/src/plugins/oSnap/components/ExternalLink.vue @@ -0,0 +1,35 @@ + + + diff --git a/src/plugins/oSnap/components/HandleOutcome/HandleOutcome.vue b/src/plugins/oSnap/components/HandleOutcome/HandleOutcome.vue new file mode 100644 index 00000000000..eab799b7bce --- /dev/null +++ b/src/plugins/oSnap/components/HandleOutcome/HandleOutcome.vue @@ -0,0 +1,402 @@ + + + diff --git a/src/plugins/oSnap/components/HandleOutcome/steps/CanProposeToOG.vue b/src/plugins/oSnap/components/HandleOutcome/steps/CanProposeToOG.vue new file mode 100644 index 00000000000..0db73643aa5 --- /dev/null +++ b/src/plugins/oSnap/components/HandleOutcome/steps/CanProposeToOG.vue @@ -0,0 +1,79 @@ + + + diff --git a/src/plugins/oSnap/components/HandleOutcome/steps/CanRequestTxExecution.vue b/src/plugins/oSnap/components/HandleOutcome/steps/CanRequestTxExecution.vue new file mode 100644 index 00000000000..7b5e7ea0406 --- /dev/null +++ b/src/plugins/oSnap/components/HandleOutcome/steps/CanRequestTxExecution.vue @@ -0,0 +1,22 @@ + + + diff --git a/src/plugins/oSnap/components/HandleOutcome/steps/InOOChallengePeriod.vue b/src/plugins/oSnap/components/HandleOutcome/steps/InOOChallengePeriod.vue new file mode 100644 index 00000000000..7c8346775e2 --- /dev/null +++ b/src/plugins/oSnap/components/HandleOutcome/steps/InOOChallengePeriod.vue @@ -0,0 +1,34 @@ + + + diff --git a/src/plugins/oSnap/components/HandleOutcome/steps/RejectedBySnapshotVote.vue b/src/plugins/oSnap/components/HandleOutcome/steps/RejectedBySnapshotVote.vue new file mode 100644 index 00000000000..ec01047eafb --- /dev/null +++ b/src/plugins/oSnap/components/HandleOutcome/steps/RejectedBySnapshotVote.vue @@ -0,0 +1,10 @@ + + + diff --git a/src/plugins/oSnap/components/HandleOutcome/steps/TallyingSnapshotVotes.vue b/src/plugins/oSnap/components/HandleOutcome/steps/TallyingSnapshotVotes.vue new file mode 100644 index 00000000000..f4d5909be2c --- /dev/null +++ b/src/plugins/oSnap/components/HandleOutcome/steps/TallyingSnapshotVotes.vue @@ -0,0 +1,7 @@ + + + diff --git a/src/plugins/oSnap/components/HandleOutcome/steps/TransactionsExecuted.vue b/src/plugins/oSnap/components/HandleOutcome/steps/TransactionsExecuted.vue new file mode 100644 index 00000000000..79d764302ff --- /dev/null +++ b/src/plugins/oSnap/components/HandleOutcome/steps/TransactionsExecuted.vue @@ -0,0 +1,7 @@ + + + diff --git a/src/plugins/oSnap/components/Input/Address.vue b/src/plugins/oSnap/components/Input/Address.vue new file mode 100644 index 00000000000..99189b069a6 --- /dev/null +++ b/src/plugins/oSnap/components/Input/Address.vue @@ -0,0 +1,50 @@ + + + diff --git a/src/plugins/oSnap/components/Input/Amount.vue b/src/plugins/oSnap/components/Input/Amount.vue new file mode 100644 index 00000000000..0a841a5c1a6 --- /dev/null +++ b/src/plugins/oSnap/components/Input/Amount.vue @@ -0,0 +1,47 @@ + + + diff --git a/src/plugins/oSnap/components/Input/MethodParameter.vue b/src/plugins/oSnap/components/Input/MethodParameter.vue new file mode 100644 index 00000000000..fe8a25ba727 --- /dev/null +++ b/src/plugins/oSnap/components/Input/MethodParameter.vue @@ -0,0 +1,141 @@ + + + diff --git a/src/plugins/oSnap/components/Input/ReadOnly.vue b/src/plugins/oSnap/components/Input/ReadOnly.vue new file mode 100644 index 00000000000..8ddb45a5d12 --- /dev/null +++ b/src/plugins/oSnap/components/Input/ReadOnly.vue @@ -0,0 +1,9 @@ + + + diff --git a/src/plugins/oSnap/components/Input/SelectSafe.vue b/src/plugins/oSnap/components/Input/SelectSafe.vue new file mode 100644 index 00000000000..5717db43d23 --- /dev/null +++ b/src/plugins/oSnap/components/Input/SelectSafe.vue @@ -0,0 +1,36 @@ + + + diff --git a/src/plugins/oSnap/components/Input/TransactionType.vue b/src/plugins/oSnap/components/Input/TransactionType.vue new file mode 100644 index 00000000000..7cadab8c09d --- /dev/null +++ b/src/plugins/oSnap/components/Input/TransactionType.vue @@ -0,0 +1,63 @@ + + + diff --git a/src/plugins/oSnap/components/SafeLinkWithAvatar.vue b/src/plugins/oSnap/components/SafeLinkWithAvatar.vue new file mode 100644 index 00000000000..d10b16086ba --- /dev/null +++ b/src/plugins/oSnap/components/SafeLinkWithAvatar.vue @@ -0,0 +1,28 @@ + + + diff --git a/src/plugins/oSnap/components/TransactionBuilder/ContractInteraction.vue b/src/plugins/oSnap/components/TransactionBuilder/ContractInteraction.vue new file mode 100644 index 00000000000..4f3b5b2ca91 --- /dev/null +++ b/src/plugins/oSnap/components/TransactionBuilder/ContractInteraction.vue @@ -0,0 +1,154 @@ + + + diff --git a/src/plugins/oSnap/components/TransactionBuilder/ModalSafe.vue b/src/plugins/oSnap/components/TransactionBuilder/ModalSafe.vue new file mode 100644 index 00000000000..3b0ac9e1950 --- /dev/null +++ b/src/plugins/oSnap/components/TransactionBuilder/ModalSafe.vue @@ -0,0 +1,43 @@ + + + diff --git a/src/plugins/oSnap/components/TransactionBuilder/ModalTransactionType.vue b/src/plugins/oSnap/components/TransactionBuilder/ModalTransactionType.vue new file mode 100644 index 00000000000..527628dd07c --- /dev/null +++ b/src/plugins/oSnap/components/TransactionBuilder/ModalTransactionType.vue @@ -0,0 +1,44 @@ + + + diff --git a/src/plugins/oSnap/components/TransactionBuilder/RawTransaction.vue b/src/plugins/oSnap/components/TransactionBuilder/RawTransaction.vue new file mode 100644 index 00000000000..0ca452a6d83 --- /dev/null +++ b/src/plugins/oSnap/components/TransactionBuilder/RawTransaction.vue @@ -0,0 +1,77 @@ + + + diff --git a/src/plugins/oSnap/components/TransactionBuilder/TokensModal.vue b/src/plugins/oSnap/components/TransactionBuilder/TokensModal.vue new file mode 100644 index 00000000000..52375acc4e6 --- /dev/null +++ b/src/plugins/oSnap/components/TransactionBuilder/TokensModal.vue @@ -0,0 +1,150 @@ + + + diff --git a/src/plugins/oSnap/components/TransactionBuilder/TokensModalItem.vue b/src/plugins/oSnap/components/TransactionBuilder/TokensModalItem.vue new file mode 100644 index 00000000000..af3166ebddd --- /dev/null +++ b/src/plugins/oSnap/components/TransactionBuilder/TokensModalItem.vue @@ -0,0 +1,76 @@ + + + diff --git a/src/plugins/oSnap/components/TransactionBuilder/Transaction.vue b/src/plugins/oSnap/components/TransactionBuilder/Transaction.vue new file mode 100644 index 00000000000..e5e7c0a5523 --- /dev/null +++ b/src/plugins/oSnap/components/TransactionBuilder/Transaction.vue @@ -0,0 +1,94 @@ + + + diff --git a/src/plugins/oSnap/components/TransactionBuilder/TransactionBuilder.vue b/src/plugins/oSnap/components/TransactionBuilder/TransactionBuilder.vue new file mode 100644 index 00000000000..622d55df811 --- /dev/null +++ b/src/plugins/oSnap/components/TransactionBuilder/TransactionBuilder.vue @@ -0,0 +1,83 @@ + + + diff --git a/src/plugins/oSnap/components/TransactionBuilder/TransferFunds.vue b/src/plugins/oSnap/components/TransactionBuilder/TransferFunds.vue new file mode 100644 index 00000000000..f0492076631 --- /dev/null +++ b/src/plugins/oSnap/components/TransactionBuilder/TransferFunds.vue @@ -0,0 +1,124 @@ + + + diff --git a/src/plugins/oSnap/components/TransactionBuilder/TransferNFT.vue b/src/plugins/oSnap/components/TransactionBuilder/TransferNFT.vue new file mode 100644 index 00000000000..f54b022c937 --- /dev/null +++ b/src/plugins/oSnap/components/TransactionBuilder/TransferNFT.vue @@ -0,0 +1,100 @@ + + + diff --git a/src/plugins/oSnap/constants.ts b/src/plugins/oSnap/constants.ts new file mode 100644 index 00000000000..4b5dc1ea5be --- /dev/null +++ b/src/plugins/oSnap/constants.ts @@ -0,0 +1,1494 @@ +export const safePrefixes = { + 1: 'eth', + 2: 'exp', + 3: 'rop', + 4: 'rin', + 5: 'gor', + 6: 'kot', + 7: 'tch', + 8: 'ubq', + 9: 'tubq', + 10: 'oeth', + 11: 'meta', + 12: 'kal', + 13: 'dstg', + 14: 'flr', + 15: 'diode', + 16: 'cflr', + 17: 'tfi', + 18: 'TST', + 19: 'sgb', + 20: 'esc', + 21: 'esct', + 22: 'eladid', + 23: 'eladidt', + 24: 'kardiachain', + 25: 'cro', + 26: 'L1test', + 27: 'shib', + 28: 'BobaRinkeby', + 29: 'L1', + 30: 'rsk', + 31: 'trsk', + 32: 'GooDT', + 33: 'GooD', + 34: 'dth', + 35: 'tbwg', + 36: 'dx', + 37: 'xpla', + 38: 'val', + 39: 'u2u', + 40: 'TelosEVM', + 41: 'TelosEVMTestnet', + 42: 'lukso', + 43: 'pangolin', + 44: 'crab', + 45: 'pangoro', + 46: 'darwinia', + 47: 'aic', + 48: 'etmp', + 49: 'etmpTest', + 50: 'xdc', + 51: 'txdc', + 52: 'cet', + 53: 'tcet', + 54: 'OP', + 55: 'ZYX', + 56: 'bnb', + 57: 'sys', + 58: 'OntologyMainnet', + 59: 'eos-legacy', + 60: 'go', + 61: 'etc', + 62: 'tetc', + 63: 'metc', + 64: 'ellaism', + 65: 'tokt', + 66: 'okt', + 67: 'dbm', + 68: 'SO1', + 69: 'okov', + 70: 'hsc', + 71: 'cfxtest', + 72: 'dxc', + 73: 'FNCY', + 74: 'idchain', + 75: 'DSC', + 76: 'mix', + 77: 'spoa', + 78: 'primuschain', + 79: 'zenith', + 80: 'GeneChain', + 81: 'joc', + 82: 'Meter', + 83: 'MeterTest', + 84: 'linqto-devnet', + 85: 'gttest', + 86: 'gt', + 87: 'nnw', + 88: 'tomo', + 89: 'tomot', + 90: 'gar-s0', + 91: 'gar-s1', + 92: 'gar-s2', + 93: 'gar-s3', + 94: 'sdlt', + 95: 'camdl', + 96: 'bkc', + 97: 'bnbt', + 98: 'six', + 99: 'poa', + 100: 'gno', + 101: 'eti', + 102: 'tw3g', + 103: 'WLC', + 104: 'tklc', + 105: 'dw3g', + 106: 'vlx', + 107: 'ntn', + 108: 'TT', + 109: 'shibariumecosystem', + 110: 'xpr', + 111: 'ETL', + 112: 'coinbit', + 113: 'deh', + 114: 'c2flr', + 115: 'debank-testnet', + 116: 'debank-mainnet', + 117: 'auptick', + 118: 'arcology', + 119: 'enuls', + 120: 'enulst', + 121: 'REAL', + 122: 'fuse', + 123: 'spark', + 124: 'dwu', + 125: 'OYchainTestnet', + 126: 'OYchainMainnet', + 127: 'feth', + 128: 'heco', + 134: 'rlc', + 135: 'AlyxTestnet', + 136: 'deam', + 137: 'matic', + 138: 'dfio-meta-main', + 139: 'woop', + 141: 'OPtest', + 142: 'dax', + 144: 'PHI', + 148: 'shimmerevm-mainnet', + 150: 'sixt', + 151: 'rbn', + 152: 'rbn-devnet', + 153: 'rbn-testnet', + 154: 'rbn-tge', + 155: 'tenet-testnet', + 156: 'obe', + 160: 'eva', + 161: 'wall-e', + 162: 'tpht', + 163: 'pht', + 165: 'omni_testnet', + 167: 'atoshi', + 168: 'aioz', + 169: 'manta', + 170: 'hoosmartchain', + 172: 'resil', + 180: 'ame', + 186: 'Seele', + 188: 'BMC', + 189: 'BMCT', + 193: 'cem', + 195: 'tokb', + 196: 'okb', + 197: 'NEUTR', + 198: 'bit', + 199: 'BTT', + 200: 'aox', + 201: 'moactest', + 204: 'obnb', + 208: 'utx', + 210: 'BTN', + 211: 'EDI', + 212: 'makalu', + 217: 'SIN2', + 218: 'SO1-old', + 222: 'ASK', + 225: 'LA', + 226: 'TLA', + 230: 'SDX', + 236: 'deamtest', + 242: 'plgchain', + 246: 'ewt', + 248: 'OAS', + 250: 'ftm', + 255: 'kroma', + 256: 'hecot', + 258: 'setm', + 259: 'neon', + 262: 'SUR', + 269: 'hpb', + 271: 'EGONm', + 274: 'lachain', + 280: 'zksync-goerli', + 288: 'Boba', + 291: 'orderly', + 295: 'hedera-mainnet', + 296: 'hedera-testnet', + 297: 'hedera-previewnet', + 298: 'hedera-localnet', + 300: 'ogc', + 301: 'Bobaopera', + 303: 'ncnt', + 309: 'wyz', + 311: 'omax', + 313: 'ncn', + 314: 'filecoin', + 321: 'kcs', + 322: 'kcst', + 324: 'zksync', + 333: 'w3q', + 335: 'DFKTEST', + 336: 'sdn', + 338: 'tcro', + 345: 'YVM', + 361: 'theta-mainnet', + 363: 'theta-sapphire', + 364: 'theta-amber', + 365: 'theta-testnet', + 369: 'pls', + 371: 'tCNT', + 385: 'lisinski', + 400: 'hpn', + 401: 'ozo_tst', + 411: 'pepe', + 416: 'SX', + 418: 'latestnet', + 420: 'ogor', + 424: 'PGN', + 427: 'zeeth', + 443: 'obs-testnet', + 444: 'synapse-sepolia', + 456: 'arzio', + 462: 'tarea', + 499: 'rupx', + 500: 'Camino', + 501: 'Columbus', + 512: 'aac', + 513: 'aact', + 516: 'gz-mainnet', + 520: 'xt', + 529: 'fire', + 530: 'FxCore', + 534: 'CNDL', + 542: 'PAW', + 555: 'CLASS', + 558: 'tao', + 568: 'dct', + 570: 'sys-rollux', + 588: 'metis-stardust', + 592: 'astr', + 595: 'maca', + 596: 'tkar', + 597: 'taca', + 599: 'metis-goerli', + 600: 'mesh-chain-testnet', + 601: 'PEER', + 614: 'glq', + 634: 'avocado', + 647: 'SX-Testnet', + 648: 'ace', + 666: 'pixie-chain-testnet', + 667: 'laos', + 668: 'junca', + 669: 'juncat', + 686: 'kar', + 700: 'SNS', + 707: 'bcs', + 708: 'tbcs', + 710: 'fury', + 719: 'shibarium', + 721: 'LYC', + 740: 'tcanto', + 741: 'vsct', + 742: 'SPAY', + 766: 'qom', + 776: 'opc', + 777: 'cth', + 786: 'maal', + 787: 'aca', + 788: 'taero', + 789: 'peth', + 800: 'LUCID', + 803: 'haic', + 808: 'PFTEST', + 813: 'meer', + 818: 'BOC', + 820: 'clo', + 821: 'tclo', + 841: 'tara', + 842: 'taratest', + 859: 'zeethdev', + 868: 'FSCMainnet', + 876: 'BNKEN', + 877: 'DXT', + 880: 'ambros', + 888: 'wan', + 900: 'gar-test-s0', + 901: 'gar-test-s1', + 902: 'gar-test-s2', + 903: 'gar-test-s3', + 909: 'PF', + 910: 'DBONE', + 917: 'tfire', + 919: 'modesep', + 927: 'ydk', + 940: 'tpls', + 941: 't2bpls', + 942: 't3pls', + 943: 't4pls', + 956: 'munode', + 963: 'btc20', + 970: 'ccn', + 971: 'Huygens', + 972: 'Ascraeus', + 977: 'yeti', + 980: 'top_evm', + 985: 'memochain', + 989: 'top', + 990: 'ELm', + 997: '5ire', + 998: 'ln', + 999: 'twan', + 1000: 'gton', + 1001: 'Baobab', + 1002: 'kai', + 1003: 'tet', + 1004: 't-ekta', + 1007: 'tnew', + 1008: 'eun', + 1010: 'EVC', + 1012: 'new', + 1022: 'sku', + 1023: 'tclv', + 1024: 'clv', + 1028: 'tbtt', + 1030: 'cfx', + 1031: 'prx', + 1038: 'bronos-testnet', + 1039: 'bronos-mainnet', + 1071: 'shimmerevm-testnet-deprecated', + 1072: 'shimmerevm-testnet', + 1079: 'mintara-testnet', + 1080: 'mintara', + 1088: 'metis-andromeda', + 1089: 'humans', + 1099: 'moac', + 1101: 'zkevm', + 1107: 'tblxq', + 1108: 'blxq', + 1111: 'wemix', + 1112: 'twemix', + 1115: 'tcore', + 1116: 'core', + 1117: 'DOGSm', + 1130: 'DFI', + 1131: 'DFI-T', + 1133: 'changi', + 1138: 'ASARt', + 1139: 'MATH', + 1140: 'tMATH', + 1149: 'Plexchain', + 1170: 'auoc', + 1177: 'sht', + 1197: 'iora', + 1201: 'avis', + 1202: 'wtt', + 1213: 'popcat', + 1214: 'enter', + 1229: 'xzo', + 1230: 'UltronTestnet', + 1231: 'UtronMainnet', + 1234: 'step', + 1243: 'ARC', + 1244: 'TARC', + 1246: 'om', + 1252: 'CICT', + 1280: 'HO', + 1284: 'mbeam', + 1285: 'mriver', + 1286: 'mrock-old', + 1287: 'mbase', + 1288: 'mrock', + 1291: 'swtr', + 1294: 'Bobabeam', + 1297: 'Bobabase', + 1311: 'TDOS', + 1314: 'alyx', + 1319: 'aia', + 1320: 'aiatestnet', + 1337: 'geth', + 1338: 'ELST', + 1339: 'ELSM', + 1353: 'CIC', + 1369: 'zafic', + 1379: 'KLC', + 1388: 'ASAR', + 1392: 'mun', + 1402: 'zkevmtest', + 1422: 'testnet-zkEVM-mango-pre-audit-upgraded', + 1433: 'RIK', + 1440: 'LAS', + 1442: 'testnet-zkEVM-mango', + 1452: 'gil', + 1455: 'CTEX', + 1501: 'chainx', + 1506: 'Sherpax', + 1507: 'SherpaxTestnet', + 1515: 'beagle', + 1559: 'tenet', + 1618: 'cate', + 1620: 'ath', + 1657: 'bta', + 1662: 'Yuma', + 1663: 'Gobi', + 1688: 'LUDAN', + 1701: 'AnytypeChain', + 1707: 'TBSI', + 1708: 'tTBSI', + 1718: 'PCM', + 1773: 'TeaParty', + 1777: 'gauss', + 1804: 'kerleano', + 1807: 'rAna', + 1818: 'cube', + 1819: 'cubet', + 1856: 'tsf', + 1875: 'wbt', + 1881: 'gitshockchain', + 1890: 'lightlink_phoenix', + 1891: 'lightlink_pegasus', + 1898: 'boya', + 1907: 'bitci', + 1908: 'tbitci', + 1945: 'onus-testnet', + 1951: 'dchain-mainnet', + 1954: 'Dexilla', + 1967: 'mtc', + 1969: 'tscs', + 1970: 'scs', + 1971: 'atlr', + 1975: 'onus-mainnet', + 1984: 'euntest', + 1985: 'satoshie', + 1986: 'satoshie_testnet', + 1987: 'egem', + 1994: 'ekta', + 1995: 'edx', + 2000: 'dc', + 2001: 'milkAda', + 2002: 'milkALGO', + 2008: 'cloudwalk_testnet', + 2009: 'cloudwalk_mainnet', + 2016: 'NetZm', + 2018: 'pmint_dev', + 2019: 'pmint_test', + 2020: 'pmint', + 2021: 'edg', + 2022: 'edgt', + 2023: 'taycan-testnet', + 2025: 'rpg', + 2031: 'cfg', + 2032: 'ncfg', + 2037: 'kiwi', + 2038: 'shraptest', + 2043: 'otp', + 2044: 'Shrapnel', + 2047: 'stos-testnet', + 2048: 'stos-mainnet', + 2049: 'movo', + 2077: 'QKA', + 2088: 'air', + 2089: 'algl', + 2100: 'eco', + 2101: 'esp', + 2109: 'exn', + 2122: 'Metad', + 2124: 'MEU', + 2137: 'bigsb', + 2138: 'dfio-meta-test', + 2151: 'boa', + 2152: 'fra', + 2153: 'findora-testnet', + 2154: 'findora-forge', + 2199: 'msn', + 2202: 'ABNm', + 2203: 'BTC', + 2213: 'evanesco', + 2221: 'tkava', + 2222: 'kava', + 2223: 'VChain', + 2241: 'KRST', + 2300: 'bomb', + 2309: 'arevia', + 2323: 'sma', + 2330: 'alt', + 2332: 'smam', + 2357: 'deprecated-kroma-sepolia', + 2358: 'kroma-sepolia', + 2399: 'bombt', + 2400: 'TCGV', + 2415: 'xodex', + 2484: 'u2u_nebulas', + 2559: 'ktoc', + 2569: 'tpc', + 2606: 'pocrnet', + 2611: 'REDLC', + 2612: 'EZChain', + 2613: 'Fuji-EZChain', + 2625: 'twbt', + 2710: 'tmorph', + 2888: 'BobaGoerli', + 2999: 'bty', + 3000: 'cennz-r', + 3001: 'cennz-n', + 3003: 'cau', + 3011: '3ULL', + 3031: 'ORL', + 3068: 'bfc', + 3141: 'filecoin-hyperspace', + 3269: 'dubx', + 3270: 'testdubx', + 3306: 'debounce-devnet', + 3331: 'zcrbeach', + 3333: 'w3q-t', + 3334: 'w3q-g', + 3400: 'prb', + 3434: 'SCAIt', + 3500: 'prbtestnet', + 3501: 'jfin', + 3601: 'pando-mainnet', + 3602: 'pando-testnet', + 3636: 'BTCt', + 3637: 'BTCm', + 3666: 'jouleverse', + 3690: 'btx', + 3693: 'empire', + 3698: 'SPCt', + 3699: 'SPCm', + 3701: 'xplatest', + 3737: 'csb', + 3797: 'alv', + 3888: 'kalymainnet', + 3889: 'kalytestnet', + 3912: 'drac', + 3939: 'dost', + 3966: 'dyno', + 3967: 'tdyno', + 3999: 'ycc', + 4000: 'ozo', + 4001: 'PERIUM', + 4002: 'tftm', + 4051: 'BobaoperaTestnet', + 4061: 'Nahmii3Mainnet', + 4062: 'Nahmii3Testnet', + 4090: 'Oasis', + 4096: 'BNIt', + 4099: 'BNIm', + 4102: 'aioz-testnet', + 4139: 'humans_testnet', + 4141: 'TPBXt', + 4181: 'PHIv1', + 4201: 'lukso-testnet', + 4242: 'nexi', + 4328: 'BobaFujiTestnet', + 4337: 'beam', + 4444: 'html', + 4460: 'orderlyl2', + 4689: 'iotex-mainnet', + 4690: 'iotex-testnet', + 4759: 'TESTMEV', + 4777: 'TBXN', + 4918: 'txvm', + 4919: 'xvm', + 4999: 'BXN', + 5000: 'mantle', + 5001: 'mantle-testnet', + 5002: 'treasurenet', + 5005: 'tntest', + 5165: 'ftn', + 5177: 'tlc', + 5197: 'es', + 5234: 'hmnd', + 5290: '_old_fire', + 5315: 'UZMI', + 5353: 'ttrn', + 5522: 'VEX', + 5551: 'Nahmii', + 5553: 'NahmiiTestnet', + 5555: 'cverse', + 5611: 'obnbt', + 5616: 'ARCT', + 5678: 'TanssiCC', + 5700: 'tsys', + 5729: 'hik', + 5758: 'satst', + 5777: 'ggui', + 5851: 'OntologyTestnet', + 5869: 'rbd', + 6065: 'TRESTEST', + 6066: 'TRESMAIN', + 6102: 'cascadia', + 6118: 'UPTN-TEST', + 6119: 'UPTN', + 6502: 'Peerpay', + 6552: 'SRC-test', + 6565: 'fox', + 6626: 'pixie-chain', + 6688: 'iris', + 6789: 'STANDm', + 6969: 'tombchain', + 6999: 'psc', + 7000: 'zetachain-mainnet', + 7001: 'zetachain-athens', + 7027: 'ELLA', + 7070: 'planq', + 7171: 'bitrock', + 7331: 'kly', + 7332: 'EON', + 7341: 'shyft', + 7484: 'raba', + 7518: 'MEV', + 7575: 'tadil', + 7576: 'adil', + 7668: 'trn-mainnet', + 7672: 'trn-porcini', + 7700: 'canto', + 7701: 'TestnetCanto', + 7771: 'tbitrock', + 7777: 'RiseOfTheWarbotsTestnet', + 7878: 'tscas', + 7895: 'ard', + 7979: 'dos', + 8000: 'teleport', + 8001: 'teleport-testnet', + 8029: 'mdgl', + 12357: 'rei', + 7363: 'dnd', + 9052: 'acrechain', + 8080: 'Liberty10', + 8081: 'Liberty20', + 8082: 'Sphinx10', + 8086: 'BitEth', + 8098: 'StreamuX', + 8131: 'meertest', + 8132: 'meermix', + 8133: 'meerpriv', + 8134: 'amana', + 8135: 'flana', + 8136: 'mizana', + 8181: 'tBOC', + 8217: 'Cypress', + 8272: 'BTON', + 8285: 'Kortho', + 8387: 'fuck', + 8453: 'base', + 8654: 'toki', + 8655: 'toki-testnet', + 8723: 'olo', + 8724: 'tolo', + 8738: 'alph', + 8768: 'tmy', + 8848: 'maro', + 8880: 'unq', + 8881: 'qtz', + 8882: 'opl', + 8883: 'sph', + 8888: 'XANAChain', + 8889: 'vsc', + 8898: 'mmt', + 8899: 'jbc', + 8989: 'gmmt', + 8995: 'berg', + 9000: 'evmos-testnet', + 9001: 'evmos', + 9012: 'brb', + 9100: 'GENEC', + 9170: '_old_tfire', + 9223: 'COF', + 9339: 'DOGSt', + 9527: 'trpg', + 9528: 'QETTest', + 9559: 'testneon', + 9700: 'MainnetDev', + 9728: 'BobaBnbTestnet', + 9768: 'NetZt', + 9779: 'pn', + 9790: 'carbon', + 9792: 'carbon-testnet', + 9818: 'tIMP', + 9819: 'IMP', + 9977: 'tMIND', + 9990: 'AGNG', + 9996: 'MIND', + 9997: 'alt-testnet', + 9999: 'myn', + 10000: 'smartbch', + 10001: 'smartbchtest', + 10024: 'gon', + 10081: 'joct', + 10086: 'SJ', + 10101: 'GEN', + 10200: 'chi', + 10201: 'PWR', + 10243: 'aa', + 10248: '0xt', + 10395: 'TWLC', + 10507: 'Jade', + 10508: 'Snow', + 10823: 'CCP', + 10946: 'quadrans', + 10947: 'quadranstestnet', + 11110: 'astra', + 11111: 'WAGMI', + 11115: 'astra-testnet', + 11119: 'hbit', + 11235: 'ISLM', + 11437: 'shyftt', + 11612: 'SRDXt', + 11888: 'SAN', + 11891: 'Arianee', + 12009: 'sats', + 12051: 'tZERO', + 12052: 'ZERO', + 12123: 'BRC', + 12306: 'fibo', + 12321: 'blgchain', + 12345: 'steptest', + 12611: 'astrzk', + 12715: 'tRIK', + 12890: 'tqnet', + 13000: 'SPS', + 13308: 'Credit', + 13337: 'beam-testnet', + 13381: 'Phoenix', + 13812: 'sus', + 14000: 'SPS-Test', + 14853: 'hmnd-t5', + 15551: 'loop', + 15555: 'TrustTestnet', + 15557: 'eos-testnet', + 16000: 'mtt', + 16001: 'mtttest', + 16507: 'Genesys', + 16688: 'nyancat', + 16718: 'airdao', + 16888: 'tivar', + 17000: 'holesky', + 17171: 'G8Cm', + 17180: 'PCT', + 17777: 'eos', + 18000: 'ZKST', + 18122: 'STN', + 18159: 'pom', + 18181: 'G8Ct', + 18686: 'MXCzkEVM', + 19011: 'HMV', + 19845: 'btcix', + 20001: 'Camelark', + 20729: 'CLOTestnet', + 20736: 'p12', + 21337: 'cennz-a', + 21816: 'omc', + 22023: 'SFL', + 22040: 'airdao-test', + 22222: 'NAUTCHAIN', + 22776: 'map', + 23006: 'ABNt', + 23118: 'opside', + 23294: 'sapphire', + 23295: 'sapphire-testnet', + 24484: 'web', + 24734: 'mintme', + 25888: 'GOLDT', + 25925: 'bkct', + 26026: 'frm', + 26600: 'HTZ', + 26863: 'OAC', + 28528: 'obgor', + 30067: 'Piece', + 30103: 'ceri', + 31102: 'esn', + 31223: 'CLDTX', + 31224: 'CLD', + 31337: 'got', + 31415: 'filecoin-wallaby', + 32520: 'Brise', + 32659: 'fsn', + 32769: 'zil', + 32990: 'zil-isolated-server', + 33101: 'zil-testnet', + 33333: 'avs', + 33385: 'zil-devnet', + 33469: 'zq2-devnet', + 35011: 'j2o', + 35441: 'q', + 35443: 'q-testnet', + 38400: 'cmrpg', + 38401: 'ttrpg', + 39797: 'nrg', + 39815: 'oho', + 41500: 'ox-beta', + 42069: 'PC', + 42161: 'arb1', + 42170: 'arb-nova', + 42220: 'celo', + 42261: 'emerald-testnet', + 42262: 'emerald', + 42801: 'GST', + 42888: 'keth', + 43110: 'avaeth', + 43113: 'Fuji', + 43114: 'avax', + 43288: 'bobaavax', + 44444: 'FREN', + 44787: 'ALFA', + 45000: 'AutobahnNetwork', + 46688: 'tfsn', + 47805: 'REI', + 49049: 'floripa', + 49088: 'tbfc', + 49797: 'tnrg', + 50001: 'LOE', + 50021: 'tgton', + 51178: 'Opside-Testnet', + 51712: 'SRDXm', + 53935: 'DFK', + 54211: 'ISLMT', + 54321: 'ToronetTestnet', + 55004: 'teth', + 55555: 'reichain', + 55556: 'trei', + 56288: 'BobaBnb', + 56789: 'VELO', + 57000: 'tsys-rollux', + 58008: 'sepPGN', + 59140: 'linea-testnet', + 59144: 'linea', + 60000: 'TKM-test0', + 60001: 'TKM-test1', + 60002: 'TKM-test2', + 60103: 'TKM-test103', + 61800: 'aium-dev', + 61803: 'Etica', + 61916: 'DoKEN', + 62320: 'BKLV', + 62621: 'mtv', + 63000: 'ecs', + 63001: 'ecs-testnet', + 65450: 'SRC', + 67390: 'mcl', + 67588: 'Cosmic', + 69420: 'cndr', + 70000: 'TKM0', + 70001: 'TKM1', + 70002: 'TKM2', + 70103: 'TKM103', + 71111: 'GuapX', + 71393: 'ckb', + 71401: 'gw-testnet-v1', + 71402: 'gw-mainnet-v1', + 73799: 'vt', + 73927: 'mvm', + 75000: 'resin', + 77238: 'fnc', + 77612: 'vscm', + 77777: 'Toronet', + 78110: 'firenze', + 78281: 'dfly', + 78430: 'amplify', + 78431: 'bulletin', + 78432: 'conduit', + 79879: 'STANDt', + 80001: 'maticmum', + 81341: 'amanatest', + 81342: 'amanamix', + 81343: 'amanapriv', + 81351: 'flanatest', + 81352: 'flanamix', + 81353: 'flanapriv', + 81361: 'mizanatest', + 81362: 'mizanamix', + 81363: 'mizanapriv', + 81720: 'qnet', + 84531: 'basegor', + 84886: 'Aerie', + 85449: 'Cyber', + 88002: 'NAUTTest', + 88880: 'chz', + 88888: 'ivar', + 90210: 'bvhl', + 91002: 'NAUT', + 92001: 'lambda-testnet', + 96970: 'mantis', + 97288: 'BobaBnbOld', + 99099: 'ELt', + 99998: 'usctest', + 99999: 'usc', + 100000: 'qkc-r', + 100001: 'qkc-s0', + 100002: 'qkc-s1', + 100003: 'qkc-s2', + 100004: 'qkc-s3', + 100005: 'qkc-s4', + 100006: 'qkc-s5', + 100007: 'qkc-s6', + 100008: 'qkc-s7', + 100009: 'vechain', + 100010: 'vechain-testnet', + 100100: 'chi1', + 101010: 'SVRNt', + 103090: 'CRFI', + 108801: 'bro', + 110000: 'qkc-d-r', + 110001: 'qkc-d-s0', + 110002: 'qkc-d-s1', + 110003: 'qkc-d-s2', + 110004: 'qkc-d-s3', + 110005: 'qkc-d-s4', + 110006: 'qkc-d-s5', + 110007: 'qkc-d-s6', + 110008: 'qkc-d-s7', + 111000: 'testsbr', + 111111: 'sbr', + 112358: 'metao', + 123456: 'dadil', + 131419: 'ETND', + 142857: 'ICPlaza', + 167004: 'taiko-a2', + 167005: 'taiko-l2', + 167006: 'taiko-l3', + 167007: 'tko-jolnir', + 188881: 'condor', + 200101: 'milkTAda', + 200202: 'milkTAlgo', + 200625: 'aka', + 201018: 'alaya', + 201030: 'alayadev', + 201804: 'myth', + 202020: 'tDSC', + 202624: 'twl-jellie', + 210425: 'platon', + 220315: 'mas', + 221230: 'reap', + 221231: 'reap-testnet', + 224168: 'TAFECO', + 230315: 'hsktest', + 234666: 'hym', + 246529: 'ats', + 246785: 'atstau', + 247253: 'saakuru-testnet', + 256256: 'cmp-mainnet', + 266256: 'gz-testnet', + 271271: 'EGONt', + 281121: 'SoChain', + 314159: 'filecoin-calibration', + 330844: 'tc', + 333331: 'avst', + 333666: 'oonetest', + 333777: 'oonedev', + 333888: 'sparta', + 333999: 'olympus', + 355113: 'Bitfinity', + 373737: 'hap-testnet', + 381931: 'metal', + 381932: 'Tahoe', + 404040: 'TPBXm', + 420420: 'KEK', + 420666: 'tKEK', + 420692: 'alterium', + 421611: 'arb-rinkeby', + 421613: 'arb-goerli', + 421614: 'arb-sep', + 424242: 'fastexTestnet', + 431140: 'markr-go', + 432201: 'dexalot-testnet', + 432204: 'dexalot', + 444900: 'wlkt', + 471100: 'psep', + 474142: 'oc', + 512512: 'cmp', + 513100: 'ethf', + 534351: 'scr-sepolia', + 534352: 'scr', + 534353: 'scr-alpha', + 534354: 'scr-prealpha', + 534849: 'shi', + 535037: 'BESC', + 622277: 'rth', + 641230: 'BRNKC', + 651940: 'ALL', + 666666: 'vpioneer', + 751230: 'BRNKCTEST', + 761412: 'Miexs', + 776877: 'mdlrm', + 800001: 'octa', + 827431: 'CURVEm', + 846000: 'bloqs4good', + 888888: 'vision', + 900000: 'psc-s0', + 910000: 'psc-t-s0', + 920000: 'psc-d-s0', + 920001: 'psc-d-s1', + 923018: 'tFNCY', + 955305: 'elv', + 1261120: 'azktn', + 1313114: 'etho', + 1313500: 'xero', + 1337702: 'kintsugi', + 1337802: 'kiln', + 1337803: 'zhejiang', + 2021398: 'dbk', + 2099156: 'plian-mainnet', + 2203181: 'platondev', + 2206132: 'platondev2', + 3141592: 'filecoin-butterfly', + 3441005: 'mantaTestnet', + 4000003: 'alt-zerogas', + 4281033: 'worldscal', + 5167003: 'MXC', + 5555555: 'imversed', + 5555558: 'imversed-testnet', + 7225878: 'saakuru', + 7355310: 'vsl', + 7668378: 'tqom', + 7762959: 'music', + 7777777: 'zora', + 8007736: 'plian-mainnet-l2', + 8794598: 'hap', + 8888881: 'quarix-testnet', + 8888888: 'quarix', + 10067275: 'plian-testnet-l2', + 10101010: 'SVRNm', + 11155111: 'sep', + 13371337: 'tpep', + 14288640: 'anduschain-mainnet', + 16658437: 'plian-testnet', + 18289463: 'ilt', + 20180430: 'spectrum', + 20181205: 'qki', + 20201022: 'pg', + 22052002: 'xlon', + 27082017: 'exlvolta', + 27082022: 'exl', + 28945486: 'auxi', + 29032022: 'fla', + 31415926: 'filecoin-local', + 35855456: 'JOYS', + 43214913: 'mais', + 61717561: 'aqua', + 65010000: 'bakerloo-0', + 65100000: 'piccadilly-0', + 88888888: 'team', + 99415706: 'TOYS', + 192837465: 'GTH', + 222000222: 'kanazawa', + 245022926: 'neonevm-devnet', + 245022934: 'neonevm-mainnet', + 245022940: 'neonevm-testnet', + 278611351: 'razor', + 311752642: 'oneledger', + 333000333: 'meld', + 344106930: 'calypso-testnet', + 356256156: 'tGTH', + 486217935: 'dGTH', + 503129905: 'nebula-staging', + 1122334455: 'ipos', + 1146703430: 'cyb', + 1273227453: 'human-mainnet', + 1313161554: 'aurora', + 1313161555: 'aurora-testnet', + 1313161556: 'aurora-betanet', + 1351057110: 'chaos-tenet', + 1380996178: 'rptr', + 1482601649: 'nebula-mainnet', + 1564830818: 'calypso-mainnet', + 1666600000: 'hmy-s0', + 1666600001: 'hmy-s1', + 1666600002: 'hmy-s2', + 1666600003: 'hmy-s3', + 1666700000: 'hmy-b-s0', + 1666700001: 'hmy-b-s1', + 1666900000: 'hmy-ps-s0', + 1666900001: 'hmy-ps-s1', + 2021121117: 'hop', + 2046399126: 'europa', + 2863311531: 'a8', + 3125659152: 'pirl', + 4216137055: 'frankenstein', + 11297108099: 'tpalm', + 11297108109: 'palm', + 111222333444: 'alphabet', + 197710212030: 'ntt', + 197710212031: 'ntt-haradev', + 383414847825: 'zeniq', + 666301171999: 'ipdc', + 6022140761023: 'mole', + 868455272153094: 'gw-testnet-v1-deprecated' +} as const; + +export const EXPLORER_API_URLS = { + '1': 'https://api.etherscan.io/api', + '5': 'https://api-goerli.etherscan.io/api', + '100': 'https://gnosis.blockscout.com/api', + '73799': 'https://volta-explorer.energyweb.org/api', + '246': 'https://explorer.energyweb.org/api', + '137': 'https://api.polygonscan.com/api', + '56': 'https://api.bscscan.com/api', + '42161': 'https://api.arbiscan.io/api' +} as const; + +export const GNOSIS_SAFE_TRANSACTION_API_URLS = { + '1': 'https://safe-transaction-mainnet.safe.global/api', + '5': 'https://safe-transaction-goerli.safe.global/api', + '100': 'https://safe-transaction-gnosis-chain.safe.global/api', + '73799': 'https://safe-transaction-volta.safe.global/api', + '246': 'https://safe-transaction-ewc.safe.global/api', + '137': 'https://safe-transaction-polygon.safe.global/api', + '56': 'https://safe-transaction-bsc.safe.global/api', + '42161': 'https://safe-transaction-arbitrum.safe.global/api' +} as const; + +// ABIs +export const OPTIMISTIC_GOVERNOR_ABI = [ + 'constructor(address _finder, address _owner, address _collateral, uint256 _bondAmount, string _rules, bytes32 _identifier, uint64 _liveness)', + 'error NotIERC165Compliant(address guard_)', + 'event AvatarSet(address indexed previousAvatar, address indexed newAvatar)', + 'event ChangedGuard(address guard)', + 'event Initialized(uint8 version)', + 'event OptimisticGovernorDeployed(address indexed owner, address indexed avatar, address target)', + 'event OptimisticOracleChanged(address indexed newOptimisticOracleV3)', + 'event OwnershipTransferred(address indexed previousOwner, address indexed newOwner)', + 'event ProposalDeleted(bytes32 indexed proposalHash, bytes32 indexed assertionId)', + 'event ProposalExecuted(bytes32 indexed proposalHash, bytes32 indexed assertionId)', + 'event SetCollateralAndBond(address indexed collateral, uint256 indexed bondAmount)', + 'event SetEscalationManager(address indexed escalationManager)', + 'event SetIdentifier(bytes32 indexed identifier)', + 'event SetLiveness(uint64 indexed liveness)', + 'event SetRules(string rules)', + 'event TargetSet(address indexed previousTarget, address indexed newTarget)', + 'event TransactionExecuted(bytes32 indexed proposalHash, bytes32 indexed assertionId, uint256 indexed transactionIndex)', + 'event TransactionsProposed(address indexed proposer, uint256 indexed proposalTime, bytes32 indexed assertionId, tuple(tuple(address to, uint8 operation, uint256 value, bytes data)[] transactions, uint256 requestTime) proposal, bytes32 proposalHash, bytes explanation, string rules, uint256 challengeWindowEnds)', + 'function EXPLANATION_KEY() view returns (bytes)', + 'function PROPOSAL_HASH_KEY() view returns (bytes)', + 'function RULES_KEY() view returns (bytes)', + 'function assertionDisputedCallback(bytes32 assertionId)', + 'function assertionIds(bytes32) view returns (bytes32)', + 'function assertionResolvedCallback(bytes32 assertionId, bool assertedTruthfully)', + 'function avatar() view returns (address)', + 'function bondAmount() view returns (uint256)', + 'function collateral() view returns (address)', + 'function deleteProposalOnUpgrade(bytes32 proposalHash)', + 'function escalationManager() view returns (address)', + 'function executeProposal(tuple(address to, uint8 operation, uint256 value, bytes data)[] transactions)', + 'function finder() view returns (address)', + 'function getCurrentTime() view returns (uint256)', + 'function getGuard() view returns (address _guard)', + 'function getProposalBond() view returns (uint256)', + 'function guard() view returns (address)', + 'function identifier() view returns (bytes32)', + 'function liveness() view returns (uint64)', + 'function optimisticOracleV3() view returns (address)', + 'function owner() view returns (address)', + 'function proposalHashes(bytes32) view returns (bytes32)', + 'function proposeTransactions(tuple(address to, uint8 operation, uint256 value, bytes data)[] transactions, bytes explanation)', + 'function renounceOwnership()', + 'function rules() view returns (string)', + 'function setAvatar(address _avatar)', + 'function setCollateralAndBond(address _collateral, uint256 _bondAmount)', + 'function setEscalationManager(address _escalationManager)', + 'function setGuard(address _guard)', + 'function setIdentifier(bytes32 _identifier)', + 'function setLiveness(uint64 _liveness)', + 'function setRules(string _rules)', + 'function setTarget(address _target)', + 'function setUp(bytes initializeParams)', + 'function sync()', + 'function target() view returns (address)', + 'function transferOwnership(address newOwner)' +] as const; + +export const OPTIMISTIC_ORACLE_V3_ABI = [ + 'constructor(address _finder, address _defaultCurrency, uint64 _defaultLiveness)', + 'event AdminPropertiesSet(address defaultCurrency, uint64 defaultLiveness, uint256 burnedBondPercentage)', + 'event AssertionDisputed(bytes32 indexed assertionId, address indexed caller, address indexed disputer)', + 'event AssertionMade(bytes32 indexed assertionId, bytes32 domainId, bytes claim, address indexed asserter, address callbackRecipient, address escalationManager, address caller, uint64 expirationTime, address currency, uint256 bond, bytes32 indexed identifier)', + 'event AssertionSettled(bytes32 indexed assertionId, address indexed bondRecipient, bool disputed, bool settlementResolution, address settleCaller)', + 'event OwnershipTransferred(address indexed previousOwner, address indexed newOwner)', + 'function assertTruth(bytes claim, address asserter, address callbackRecipient, address escalationManager, uint64 liveness, address currency, uint256 bond, bytes32 identifier, bytes32 domainId) returns (bytes32 assertionId)', + 'function assertTruthWithDefaults(bytes claim, address asserter) returns (bytes32)', + 'function assertions(bytes32) view returns (tuple(bool arbitrateViaEscalationManager, bool discardOracle, bool validateDisputers, address assertingCaller, address escalationManager) escalationManagerSettings, address asserter, uint64 assertionTime, bool settled, address currency, uint64 expirationTime, bool settlementResolution, bytes32 domainId, bytes32 identifier, uint256 bond, address callbackRecipient, address disputer)', + 'function burnedBondPercentage() view returns (uint256)', + 'function cachedCurrencies(address) view returns (bool isWhitelisted, uint256 finalFee)', + 'function cachedIdentifiers(bytes32) view returns (bool)', + 'function cachedOracle() view returns (address)', + 'function defaultCurrency() view returns (address)', + 'function defaultIdentifier() view returns (bytes32)', + 'function defaultLiveness() view returns (uint64)', + 'function disputeAssertion(bytes32 assertionId, address disputer)', + 'function finder() view returns (address)', + 'function getAssertion(bytes32 assertionId) view returns (tuple(tuple(bool arbitrateViaEscalationManager, bool discardOracle, bool validateDisputers, address assertingCaller, address escalationManager) escalationManagerSettings, address asserter, uint64 assertionTime, bool settled, address currency, uint64 expirationTime, bool settlementResolution, bytes32 domainId, bytes32 identifier, uint256 bond, address callbackRecipient, address disputer))', + 'function getAssertionResult(bytes32 assertionId) view returns (bool)', + 'function getCurrentTime() view returns (uint256)', + 'function getMinimumBond(address currency) view returns (uint256)', + 'function multicall(bytes[] data) returns (bytes[] results)', + 'function numericalTrue() view returns (int256)', + 'function owner() view returns (address)', + 'function renounceOwnership()', + 'function setAdminProperties(address _defaultCurrency, uint64 _defaultLiveness, uint256 _burnedBondPercentage)', + 'function settleAndGetAssertionResult(bytes32 assertionId) returns (bool)', + 'function settleAssertion(bytes32 assertionId)', + 'function stampAssertion(bytes32 assertionId) view returns (bytes)', + 'function syncUmaParams(bytes32 identifier, address currency)', + 'function transferOwnership(address newOwner)' +] as const; + +export const VOTING_ABI = [ + 'constructor(uint128 _emissionRate, uint64 _unstakeCoolDown, uint64 _phaseLength, uint32 _maxRolls, uint32 _maxRequestsPerRound, uint128 _gat, uint64 _spat, address _votingToken, address _finder, address _slashingLibrary, address _previousVotingContract)', + 'event DelegateSet(address indexed delegator, address indexed delegate)', + 'event DelegatorSet(address indexed delegate, address indexed delegator)', + 'event EncryptedVote(address indexed caller, uint32 indexed roundId, bytes32 indexed identifier, uint256 time, bytes ancillaryData, bytes encryptedVote)', + 'event ExecutedUnstake(address indexed voter, uint128 tokensSent, uint128 voterStake)', + 'event GatAndSpatChanged(uint128 newGat, uint64 newSpat)', + 'event MaxRequestsPerRoundChanged(uint32 newMaxRequestsPerRound)', + 'event MaxRollsChanged(uint32 newMaxRolls)', + 'event OwnershipTransferred(address indexed previousOwner, address indexed newOwner)', + 'event RequestAdded(address indexed requester, uint32 indexed roundId, bytes32 indexed identifier, uint256 time, bytes ancillaryData, bool isGovernance)', + 'event RequestDeleted(bytes32 indexed identifier, uint256 indexed time, bytes ancillaryData, uint32 rollCount)', + 'event RequestResolved(uint32 indexed roundId, uint256 indexed resolvedPriceRequestIndex, bytes32 indexed identifier, uint256 time, bytes ancillaryData, int256 price)', + 'event RequestRolled(bytes32 indexed identifier, uint256 indexed time, bytes ancillaryData, uint32 rollCount)', + 'event RequestedUnstake(address indexed voter, uint128 amount, uint64 unstakeTime, uint128 voterStake)', + 'event SetNewEmissionRate(uint128 newEmissionRate)', + 'event SetNewUnstakeCoolDown(uint64 newUnstakeCoolDown)', + 'event SlashingLibraryChanged(address newAddress)', + 'event Staked(address indexed voter, address indexed from, uint128 amount, uint128 voterStake, uint128 voterPendingUnstake, uint128 cumulativeStake)', + 'event UpdatedReward(address indexed voter, uint128 newReward, uint64 lastUpdateTime)', + 'event VoteCommitted(address indexed voter, address indexed caller, uint32 roundId, bytes32 indexed identifier, uint256 time, bytes ancillaryData)', + 'event VoteRevealed(address indexed voter, address indexed caller, uint32 roundId, bytes32 indexed identifier, uint256 time, bytes ancillaryData, int256 price, uint128 numTokens)', + 'event VoterSlashApplied(address indexed voter, int128 slashedTokens, uint128 postStake)', + 'event VoterSlashed(address indexed voter, uint256 indexed requestIndex, int128 slashedTokens)', + 'event VotingContractMigrated(address newAddress)', + 'event WithdrawnRewards(address indexed voter, address indexed delegate, uint128 tokensWithdrawn)', + 'function ANCILLARY_BYTES_LIMIT() view returns (uint256)', + 'function UINT64_MAX() view returns (uint64)', + 'function commitAndEmitEncryptedVote(bytes32 identifier, uint256 time, bytes ancillaryData, bytes32 hash, bytes encryptedVote)', + 'function commitVote(bytes32 identifier, uint256 time, bytes ancillaryData, bytes32 hash)', + 'function cumulativeStake() view returns (uint128)', + 'function currentActiveRequests() view returns (bool)', + 'function delegateToStaker(address) view returns (address)', + 'function emissionRate() view returns (uint128)', + 'function executeUnstake()', + 'function finder() view returns (address)', + 'function gat() view returns (uint128)', + 'function getCurrentRoundId() view returns (uint32)', + 'function getCurrentTime() view returns (uint256)', + 'function getNumberOfPriceRequests() view returns (uint256 numberPendingPriceRequests, uint256 numberResolvedPriceRequests)', + 'function getNumberOfPriceRequestsPostUpdate() returns (uint256 numberPendingPriceRequests, uint256 numberResolvedPriceRequests)', + 'function getPendingRequests() view returns (tuple(uint32 lastVotingRound, bool isGovernance, uint64 time, uint32 rollCount, bytes32 identifier, bytes ancillaryData)[])', + 'function getPrice(bytes32 identifier, uint256 time, bytes ancillaryData) view returns (int256)', + 'function getPrice(bytes32 identifier, uint256 time) view returns (int256)', + 'function getPriceRequestStatuses(tuple(bytes32 identifier, uint256 time, bytes ancillaryData)[] requests) view returns (tuple(uint8 status, uint32 lastVotingRound)[])', + 'function getRoundEndTime(uint256 roundId) view returns (uint256)', + 'function getRoundIdToVoteOnRequest(uint32 targetRoundId) view returns (uint32)', + 'function getVotePhase() view returns (uint8)', + 'function getVoterFromDelegate(address caller) view returns (address)', + 'function getVoterParticipation(uint256 requestIndex, uint32 lastVotingRound, address voter) view returns (uint8)', + 'function getVoterPendingStake(address voter, uint32 roundId) view returns (uint128)', + 'function getVoterStakePostUpdate(address voter) returns (uint128)', + 'function hasPrice(bytes32 identifier, uint256 time) view returns (bool)', + 'function hasPrice(bytes32 identifier, uint256 time, bytes ancillaryData) view returns (bool)', + 'function lastRoundIdProcessed() view returns (uint32)', + 'function lastUpdateTime() view returns (uint64)', + 'function maxRequestsPerRound() view returns (uint32)', + 'function maxRolls() view returns (uint32)', + 'function migratedAddress() view returns (address)', + 'function multicall(bytes[] data) returns (bytes[] results)', + 'function nextPendingIndexToProcess() view returns (uint64)', + 'function outstandingRewards(address voter) view returns (uint256)', + 'function owner() view returns (address)', + 'function pendingPriceRequestsIds(uint256) view returns (bytes32)', + 'function previousVotingContract() view returns (address)', + 'function priceRequests(bytes32) view returns (uint32 lastVotingRound, bool isGovernance, uint64 time, uint32 rollCount, bytes32 identifier, bytes ancillaryData)', + 'function processResolvablePriceRequests()', + 'function processResolvablePriceRequestsRange(uint64 maxTraversals)', + 'function renounceOwnership()', + 'function requestGovernanceAction(bytes32 identifier, uint256 time, bytes ancillaryData)', + 'function requestPrice(bytes32 identifier, uint256 time, bytes ancillaryData)', + 'function requestPrice(bytes32 identifier, uint256 time)', + 'function requestSlashingTrackers(uint256 requestIndex) view returns (tuple(uint256 wrongVoteSlashPerToken, uint256 noVoteSlashPerToken, uint256 totalSlashed, uint256 totalCorrectVotes, uint32 lastVotingRound))', + 'function requestUnstake(uint128 amount)', + 'function resolvedPriceRequestIds(uint256) view returns (bytes32)', + 'function retrieveRewardsOnMigratedVotingContract(address voter, uint256 roundId, tuple(bytes32 identifier, uint256 time, bytes ancillaryData)[] toRetrieve) returns (uint256)', + 'function revealVote(bytes32 identifier, uint256 time, int256 price, bytes ancillaryData, int256 salt)', + 'function rewardPerToken() view returns (uint256)', + 'function rewardPerTokenStored() view returns (uint128)', + 'function rounds(uint256) view returns (address slashingLibrary, uint128 minParticipationRequirement, uint128 minAgreementRequirement, uint128 cumulativeStakeAtRound, uint32 numberOfRequestsToVote)', + 'function setDelegate(address delegate)', + 'function setDelegator(address delegator)', + 'function setEmissionRate(uint128 newEmissionRate)', + 'function setGatAndSpat(uint128 newGat, uint64 newSpat)', + 'function setMaxRequestPerRound(uint32 newMaxRequestsPerRound)', + 'function setMaxRolls(uint32 newMaxRolls)', + 'function setMigrated(address newVotingAddress)', + 'function setSlashingLibrary(address _newSlashingLibrary)', + 'function setUnstakeCoolDown(uint64 newUnstakeCoolDown)', + 'function slashingLibrary() view returns (address)', + 'function spat() view returns (uint64)', + 'function stake(uint128 amount)', + 'function stakeTo(address recipient, uint128 amount)', + 'function transferOwnership(address newOwner)', + 'function unstakeCoolDown() view returns (uint64)', + 'function updateTrackers(address voter)', + 'function updateTrackersRange(address voter, uint64 maxTraversals)', + 'function voteTiming() view returns (uint256 phaseLength)', + 'function voterStakes(address) view returns (uint128 stake, uint128 pendingUnstake, uint128 rewardsPaidPerToken, uint128 outstandingRewards, int128 unappliedSlash, uint64 nextIndexToProcess, uint64 unstakeTime, address delegate)', + 'function votingToken() view returns (address)', + 'function withdrawAndRestake() returns (uint128)', + 'function withdrawRewards() returns (uint128)' +] as const; + +export const UMA_FINDER_ABI = [ + 'event InterfaceImplementationChanged(bytes32 indexed interfaceName, address indexed newImplementationAddress)', + 'event OwnershipTransferred(address indexed previousOwner, address indexed newOwner)', + 'function changeImplementationAddress(bytes32 interfaceName, address implementationAddress)', + 'function getImplementationAddress(bytes32 interfaceName) view returns (address)', + 'function interfacesImplemented(bytes32) view returns (address)', + 'function owner() view returns (address)', + 'function renounceOwnership()', + 'function transferOwnership(address newOwner)' +] as const; + +export const ERC20_ABI = [ + //Read functions + 'function balanceOf(address account) view returns (uint256)', + 'function decimals() view returns (uint32)', + 'function symbol() view returns (string)', + 'function allowance(address owner, address spender) external view returns (uint256)', + + // Write functions + 'function approve(address spender, uint256 value) external returns (bool)', + 'function transfer(address recipient, uint256 amount) public virtual override returns (bool)' +] as const; + +export const ERC721_ABI = [ + 'function safeTransferFrom(address _from, address _to, uint256 _tokenId) external payable' +] as const; + +// to potentially cut down on event ranges we query, hard code some deploy blocks for contracts +export type ContractData = { + network: string; + name: string; + address?: string; + deployBlock?: number; + subgraph?: string; +}; +// contract addresses pulled from https://github.com/UMAprotocol/protocol/tree/master/packages/core/networks +export const contractData = [ + { + // mainnet + network: '1', + name: 'OptimisticOracleV3', + address: '0xfb55F43fB9F48F63f9269DB7Dde3BbBe1ebDC0dE', + subgraph: + 'https://api.thegraph.com/subgraphs/name/umaprotocol/mainnet-optimistic-oracle-v3', + deployBlock: 16636058 + }, + { + // goerli + network: '5', + name: 'OptimisticOracleV3', + address: '0x9923D42eF695B5dd9911D05Ac944d4cAca3c4EAB', + subgraph: + 'https://api.thegraph.com/subgraphs/name/md0x/goerli-optimistic-oracle-v3', + deployBlock: 8497481 + }, + { + // optimism + network: '10', + name: 'OptimisticOracleV3', + address: '0x072819Bb43B50E7A251c64411e7aA362ce82803B', + subgraph: + 'https://api.thegraph.com/subgraphs/name/umaprotocol/optimism-optimistic-oracle-v3', + deployBlock: 74537234 + }, + { + // gnosis + network: '100', + name: 'OptimisticOracleV3', + address: '0x22A9AaAC9c3184f68C7B7C95b1300C4B1D2fB95C', + subgraph: + 'https://api.thegraph.com/subgraphs/name/umaprotocol/gnosis-optimistic-oracle-v3', + deployBlock: 27087150 + }, + { + // polygon + network: '137', + name: 'OptimisticOracleV3', + address: '0x5953f2538F613E05bAED8A5AeFa8e6622467AD3D', + subgraph: + 'https://api.thegraph.com/subgraphs/name/umaprotocol/polygon-optimistic-oracle-v3', + deployBlock: 39331673 + }, + { + //arbitrum + network: '42161', + name: 'OptimisticOracleV3', + address: '0xa6147867264374F324524E30C02C331cF28aa879', + subgraph: + 'https://api.thegraph.com/subgraphs/name/umaprotocol/arbitrum-optimistic-oracle-v3', + deployBlock: 61236565 + }, + { + // avalanche + network: '43114', + name: 'OptimisticOracleV3', + address: '0xa4199d73ae206d49c966cF16c58436851f87d47F', + subgraph: + 'https://api.thegraph.com/subgraphs/name/umaprotocol/avalanche-optimistic-oracle-v3', + deployBlock: 27816737 + }, + { + // mainnet + network: '1', + name: 'OptimisticGovernor', + address: '0x28CeBFE94a03DbCA9d17143e9d2Bd1155DC26D5d', + subgraph: + 'https://api.thegraph.com/subgraphs/name/umaprotocol/mainnet-optimistic-governor', + deployBlock: 16890621 + }, + // Keep in mind, OG addresses are not the module addresses for each individual space, these addresses typically + // are not used, but are here for reference. + { + //goerli + network: '5', + name: 'OptimisticGovernor', + address: '0x07a7Be7AA4AaD42696A17e974486cb64A4daC47b', + deployBlock: 8700589, + subgraph: + 'https://api.thegraph.com/subgraphs/name/md0x/goerli-optimistic-governor' + }, + { + // optimism + network: '10', + name: 'OptimisticGovernor', + address: '0x357fe84E438B3150d2F68AB9167bdb8f881f3b9A', + deployBlock: 83168480, + subgraph: + 'https://api.thegraph.com/subgraphs/name/umaprotocol/optimism-optimistic-governor' + }, + { + // gnosis + network: '100', + name: 'OptimisticGovernor', + deployBlock: 27102135, + subgraph: + 'https://api.thegraph.com/subgraphs/name/umaprotocol/gnosis-optimistic-governor' + }, + { + // polygon + network: '137', + name: 'OptimisticGovernor', + address: '0x3Cc4b597E9c3f51288c6Cd0c087DC14c3FfdD966', + deployBlock: 40677035, + subgraph: + 'https://api.thegraph.com/subgraphs/name/umaprotocol/polygon-optimistic-governor' + }, + { + // arbitrum + network: '42161', + name: 'OptimisticGovernor', + address: '0x30679ca4ea452d3df8a6c255a806e08810321763', + deployBlock: 72850751, + subgraph: + 'https://api.thegraph.com/subgraphs/name/umaprotocol/arbitrum-optimistic-governor' + }, + { + // avalanche + network: '43114', + name: 'OptimisticGovernor', + address: '0xEF8b46765ae805537053C59f826C3aD61924Db45', + deployBlock: 28050250, + subgraph: + 'https://api.thegraph.com/subgraphs/name/umaprotocol/avalanche-optimistic-governor' + } +] as const; + +export const transactionTypes = [ + 'transferFunds', + 'transferNFT', + 'contractInteraction', + 'raw' +] as const; + +export const solidityZeroHexString = + '0x0000000000000000000000000000000000000000000000000000000000000000'; diff --git a/src/plugins/oSnap/logo.png b/src/plugins/oSnap/logo.png new file mode 100644 index 00000000000..39705723381 Binary files /dev/null and b/src/plugins/oSnap/logo.png differ diff --git a/src/plugins/oSnap/plugin.json b/src/plugins/oSnap/plugin.json new file mode 100644 index 00000000000..ed13b584486 --- /dev/null +++ b/src/plugins/oSnap/plugin.json @@ -0,0 +1,12 @@ +{ + "name": "oSnap by UMA", + "version": "1.0.0", + "author": "UMA", + "website": "https://safe.global", + "icon": "ipfs://QmQjZaheMTLRrk22mrGCkGk2LNoNqqYMR8Bxtwa8mBHyc9", + "defaults": { + "proposal": { + "safe": null + } + } +} diff --git a/src/plugins/oSnap/types.ts b/src/plugins/oSnap/types.ts new file mode 100644 index 00000000000..e42238ed805 --- /dev/null +++ b/src/plugins/oSnap/types.ts @@ -0,0 +1,407 @@ +import { BigNumber } from '@ethersproject/bignumber'; +import { Contract, Event } from '@ethersproject/contracts'; +import networks from '@snapshot-labs/snapshot.js/src/networks.json'; +import { safePrefixes, transactionTypes } from './constants'; + +/** + * Represents details about the chains that snapshot supports as described in the `networks` json file. + * + * This corresponds to one of the chain ids that snapshot supports. + * + * @see `@snapshot-labs/snapshot.js/src/networks.json`. + */ +type Networks = typeof networks; + +/** + * One of the supported networks as defined in `@snapshot-labs/snapshot.js/src/networks.json`. + * @see Networks + */ +export type Network = keyof Networks; + +/** + * The supported network prefixes as defined in EIP-3770 used by Safe apps. + * @see https://eips.ethereum.org/EIPS/eip-3770 + */ +export type SafeNetworkPrefixes = typeof safePrefixes; + +/** + * One of the supported network prefixes as defined in EIP-3770 used by Safe apps. + * @see SafeNetworkPrefixes + */ +export type SafeNetworkPrefix = SafeNetworkPrefixes[Network]; + +/** + * Represents the four different types of transactions that oSnap supports. + * + * - Transfer Funds + * - Transfer NFT (also called Collectable) + * - Contract Interaction + * - Raw + */ +export type TransactionTypes = typeof transactionTypes; + +/** + * One of the four different types of transactions that oSnap supports. + * + * - Transfer Funds + * - Transfer NFT (also called Collectable) + * - Contract Interaction + * - Raw + */ +export type TransactionType = TransactionTypes[number]; + +/** + * The Optimistic Governor contract requires transactions that it executes to be formatted like this. + * + * This corresponds to the values found in the `Transaction` struct in the Optimistic Governor contract. + * + * @see https://github.com/UMAprotocol/protocol/blob/master/packages/core/contracts/optimistic-governor/implementation/OptimisticGovernor.sol + * + * NOTE: Since we don't support the contract's `delegatecall` feature in oSnap, the value for `operation` is always 0. + */ +export type OptimisticGovernorTransaction = [ + to: string, + operation: 0, + value: string, + data: string +]; + +/** + * Represents the data associated with the different types of transactions that oSnap supports. + * + * This is a discriminated union of the four object types that represent the data associated with a given transaction. The union discriminates on the `type` property of the objects. + * + * @see TransactionTypes + */ +export type Transaction = + | RawTransaction + | ContractInteractionTransaction + | TransferNftTransaction + | TransferFundsTransaction; + +/** + * Represents the fields that all transactions share. + * + * All the transaction types inherit from this type, adding their `type` field and any additional fields that they require. + * + * NOTE: the `formatted` field is what is actually sent to the Optimistic Governor contract. + */ +export type BaseTransaction = { + to: string; + value: string; + data: string; + formatted: OptimisticGovernorTransaction; +}; + +/** + * Represents a 'raw' transaction that does not have any additional fields. + */ +export type RawTransaction = BaseTransaction & { + type: 'raw'; +}; + +/** + * Represents a transaction that interacts with an arbitrary contract. + * + * @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 ContractInteractionTransaction = BaseTransaction & { + type: 'contractInteraction'; + abi?: string; + methodName?: string; + parameters?: string[]; +}; + +/** + * Represents a transaction that transfers an NFT (also called a Collectable). + * + * @field `recipient` field is the address of the recipient of the NFT. + * + * @field `collectable` field is the NFT that is being transferred. + */ +export type TransferNftTransaction = BaseTransaction & { + type: 'transferNFT'; + recipient?: string; + collectable?: NFT; +}; + +/** + * Represents a transaction that transfers funds. + * + * @field `amount` field is the amount of funds that are being transferred. + * + * @field `recipient` field is the address of the recipient of the funds. + * + * @field `token` field is the token that is being transferred. + */ +export type TransferFundsTransaction = BaseTransaction & { + type: 'transferFunds'; + amount?: string; + recipient?: string; + token?: Token; +}; + +/** + * The base type for assets that can be transferred by a transaction. + * + * Can represent either an NFT or an ERC20 token. + * + * @field `name` field is the name of the asset. + * @field `address` field is the address of the asset. + * @field `logoUri` field is the URI of the logo of the asset, if one exists. + * @field `imageUri` field is the URI of the image of the asset, if one exists. + * + * @see https://miyauchi.dev/posts/typescript-literal-hack/ for details about the `(string & {})` syntax. + */ +export type Asset = { + name: string; + address: 'main' | (string & {}); + logoUri?: string; + imageUri?: string; +}; + +/** + * Represents an ERC20 token. + * + * @field `symbol` field is the symbol of the token. + * @field `decimals` field is the number of decimals that the token has. + * @field `balance` field is the balance of the token that the user has. + * @field `verified` field is whether or not the token is verified by Gnosis or Snapshot contract. + * @field `chainId` field is the chain id of the network that the token is on. + */ +export type Token = Asset & { + symbol: string; + decimals: number; + balance?: string; + verified?: boolean; + chainId?: Network; +}; + +/** + * Represents an ERC-721 NFT (also called a Collectable). + * + * @field `id` field is the id of the NFT, usually used as the mint number. + * @field `tokenName` field is the name of the NFT. + */ +export type NFT = Asset & { + id: string; + tokenName?: string; +}; + +/** + * Represents the response from the Gnosis Safe API when fetching the balances of the tokens that the user has. This is immediately transformed after fetching into the `Token` type, which holds both the token and the balance. + * + * @field `tokenAddress` field is the address of the token. + * @field `token` field is the token that the safe has. + * @field `balance` field is the balance of the token that the user has. + */ +export type BalanceResponse = { + tokenAddress: string; + token: { + decimals: number; + logoUri: string; + name: string; + symbol: string; + }; + balance: string; +}; + +/** + * Represents the data associated with a safe. + * + * The plugin persists one object with this shape per proposal created. This object holds the `transactions` that the Optimistic Governor contract will execute. + * + * @field `safeName` field is the name of the safe. + * @field `safeAddress` field is the address of the safe. + * @field `network` field is the id for network that the safe is on. + * @field `moduleAddress` field is the address of the Optimistic Governor contract that was deployed for this safe. + * @field `transactions` field is the list of transactions that the Optimistic Governor contract will execute. + */ +export type GnosisSafe = { + safeName: string; + safeAddress: string; + network: Network; + moduleAddress: string; + transactions: Transaction[]; +}; + +/** + * Represents the data associated with the plugin. + * + * Holds one object with this shape per proposal created. This is the shape of the data that is persisted by the plugin. + * + * `safe` is null when first creating a plugin, but is then immediately populated once the user picks a safe. + * + * @field `safe` field is the safe that the plugin is currently working with. + */ +export type OsnapPluginData = { + safe: GnosisSafe | null; +}; + +/** + * Represents the data associated with an assertion on the Optimistic Oracle V3 subgraph. + * + * @field `assertionId` field is the id of the assertion. + * @field `expirationTime` field is the time that the assertion's challenge period ends. + * @field `assertionHash` field is the transaction hash from when the assertion was made. + * @field `settlementHash` field is the transaction hash from when the assertion was settled. + * @field `disputeHash` field is the transaction hash from when the assertion was disputed. + * @field `assertionLogIndex` field is the log index of the transaction from when the assertion was made. + * @field `settlementResolution` field is whether or not the assertion was resolved in favor of the asserter. + */ +export type AssertionGql = { + assertionId: string; + expirationTime: string; + assertionHash: string; + settlementHash: string | null; + disputeHash: string | null; + assertionLogIndex: string; + settlementResolution: boolean | null; +}; + +/** + * Represents the configuration of the Optimistic Governor module contract that was deployed for a given Safe. + * + * @field `moduleAddress` field is the address of the specific Optimistic Governor module contract that was deployed for a given Safe. + * @field `oracleAddress` field is the address of the Optimistic Oracle V3 contract. + * @field `rules` rules for this Optimistic Governor contract. + * @field `minimumBond` field is the minimum bond that is required for an assertion to be made on this Optimistic Governor contract. + * @field `challengePeriod` field is the challenge period that is required for an assertion to be made on this Optimistic Governor contract. + */ +export type OGModuleDetails = { + moduleAddress: string; + oracleAddress: string; + rules: string; + minimumBond: BigNumber; + challengePeriod: BigNumber; +}; + +/** + * Represents the collateral configuration for a given Optimistic Governor contract. + * + * @field `erc20Contract` field is the ERC20 contract that is used for collateral. + * @field `address` field is the address of the ERC20 contract that is used for collateral. + * @field `symbol` field is the symbol of the ERC20 contract that is used for collateral. + * @field `decimals` field is the number of decimals that the ERC20 contract that is used for collateral has. + */ +export type CollateralDetails = { + erc20Contract: Contract; + address: string; + symbol: string; + decimals: BigNumber; +}; + +/** + * Event fired when an assertion is made on the Optimistic Oracle V3 contract. + * + * @field `assertionId` field is the id of the assertion. + * @field `domainId` field is the domain id of the assertion. + * @field `claim` field is the claim of the assertion. + * @field `asserter` field is the address of the asserter. + * @field `callbackRecipient` field is the address of the callback recipient. + * @field `escalationManager` field is the address of the escalation manager. + * @field `caller` field is the address of the caller. + * @field `expirationTime` field is the time that the assertion's challenge period ends. + * @field `currency` field is the currency that the assertion is made in. + * @field `bond` field is the bond that is required for the assertion. + * @field `identifier` field is the identifier of the assertion. + */ +export type AssertionMadeEvent = Event & { + args: { + assertionId: string; // indexed + domainId: string; + claim: string; + asserter: string; // indexed + callbackRecipient: string; + escalationManager: string; + caller: string; + expirationTime: BigNumber; + currency: string; + bond: BigNumber; + identifier: string; // indexed + }; +}; + +/** + * Event fired when transactions are proposed on the Optimistic Governor contract. + * + * @field `proposer` field is the address of the proposer. + * @field `proposalTime` field is the time that the proposal was made. + * @field `assertionId` field is the id of the assertion. + * @field `proposal` field is the proposal that was made. + * @field `proposalHash` field is the hash of the proposal. + * @field `explanation` field is the explanation of the proposal, which in the case of oSnap is the ipfs url. + * @field `rules` field is the rules of the proposal. + * @field `challengeWindowEvents` field is the challenge window events of the proposal. + */ +export type TransactionsProposedEvent = Event & { + args: { + proposer: string; // indexed + proposalTime: BigNumber; // indexed + assertionId: string; // indexed + proposal: { + transactions: OptimisticGovernorTransaction[]; + requestTime: BigNumber; + }; + proposalHash: string; + explanation: string; + rules: string; + challengeWindowEvents: BigNumber; + }; +}; + +/** + * Event fired when an Optimistic Governor proposal's transactions are executed successfully. + * + * @field `proposalHash` field is the hash of the proposal. + * @field `assertionId` field is the id of the assertion. + */ +export type ProposalExecutedEvent = Event & { + args: { + proposalHash: string; // indexed + assertionId: string; // indexed + }; +}; + +/** + * Represents the transaction hash and log index of an `AssertionMade` event. + * + * We need these for generating a link to the assertion on the Optimistic Oracle dapp. + */ +export type AssertionTransactionDetails = { + assertionHash: string; + assertionLogIndex: string; +}; + +/** + * Represents the state of a proposal on the Optimistic Governor contract. When an assertion is associated with the proposal, we also include the assertion transaction hash and log index so that we can create a link to the assertion on the Optimistic Oracle dapp. + * + * There are four states that a proposal can be in: + * + * - `can-propose-to-og`: The user can propose transactions to the Optimistic Governor contract. This is the initial state of a proposal. We also indicate if this proposal has been disputed in the Oracle, so that we can warn the user to exercise caution and avoid losing their bond. + * + * - `in-oo-challenge-period`: The user has proposed transactions to the Optimistic Governor contract, and the proposal is currently in the challenge period on the Optimistic Oracle contract. We also indicate when the challenge period ends, so that we can warn the user to wait until the challenge period ends before proposing new transactions. + * + * - `can-request-tx-execution`: The user has proposed transactions to the Optimistic Governor contract, and the challenge period has ended. The user can now request that the Optimistic Governor contract execute the transactions. + * + * - `transactions-executed`: The user has proposed transactions to the Optimistic Governor contract, the challenge period has ended, and the transactions have been executed by the Optimistic Governor contract. + */ +export type OGProposalState = + | { + status: 'can-propose-to-og'; + isDisputed: boolean; + } + | (AssertionTransactionDetails & { + status: 'in-oo-challenge-period'; + expirationTime: number; + }) + | (AssertionTransactionDetails & { + status: 'can-request-tx-execution'; + }) + | (AssertionTransactionDetails & { + status: 'transactions-executed'; + }); diff --git a/src/plugins/oSnap/utils/abi.ts b/src/plugins/oSnap/utils/abi.ts new file mode 100644 index 00000000000..57451a27c16 --- /dev/null +++ b/src/plugins/oSnap/utils/abi.ts @@ -0,0 +1,158 @@ +import { FunctionFragment, Interface, ParamType } from '@ethersproject/abi'; +import { BigNumberish } from '@ethersproject/bignumber'; +import { memoize } from 'lodash'; +import { ERC20_ABI, ERC721_ABI, EXPLORER_API_URLS } from '../constants'; +import { + mustBeEthereumAddress, + mustBeEthereumContractAddress +} from './validators'; + +/** + * Checks if the `parameter` of a contract method `method` takes an array or tuple as input, based on the `baseType` of the parameter. + * + * If this is the case, we must parse the value as JSON and verify that it is valid. + */ +export function isArrayParameter(parameter: string): boolean { + return ['tuple', 'array'].includes(parameter); +} + +const fetchContractABI = memoize( + async (url: string, contractAddress: string) => { + const params = new URLSearchParams({ + module: 'contract', + action: 'getAbi', + address: contractAddress + }); + + const response = await fetch(`${url}?${params}`); + + if (!response.ok) { + return { status: 0, result: '' }; + } + + return response.json(); + }, + (url, contractAddress) => `${url}_${contractAddress}` +); + +/** + * Returns the ABI of a contract at the given address + */ +export async function getContractABI( + network: string, + contractAddress: string +): Promise { + const apiUrl = EXPLORER_API_URLS[network as keyof typeof EXPLORER_API_URLS]; + + if (!apiUrl) { + return ''; + } + + const isEthereumAddress = mustBeEthereumAddress(contractAddress); + const isEthereumContractAddress = await mustBeEthereumContractAddress( + network, + contractAddress + ); + + if (!isEthereumAddress || !isEthereumContractAddress) { + return ''; + } + + try { + const { result, status } = await fetchContractABI(apiUrl, contractAddress); + + if (status === '0') { + return ''; + } + + return result; + } catch (e) { + console.error('Failed to retrieve ABI', e); + return ''; + } +} + +/** + * Checks if a method is a write function. + * + * Only write functions can be executed by the Optimistic Governor. + */ +export function isWriteFunction(method: FunctionFragment) { + if (!method.stateMutability) return true; + return !['view', 'pure'].includes(method.stateMutability); +} + +/** + * Returns the write functions of a contract ABI. + */ +export function getABIWriteFunctions(abi: string) { + const abiInterface = new Interface(abi); + return ( + abiInterface.fragments + // Return only contract's functions + .filter(FunctionFragment.isFunctionFragment) + .map(FunctionFragment.fromObject) + // Return only write functions + .filter(isWriteFunction) + // Sort by name + .sort((a, b) => (a.name.toLowerCase() > b.name.toLowerCase() ? 1 : -1)) + ); +} + +/** + * Handles the extraction of the method's arguments from the `values` array. + * + * If the parameter is an array or tuple, we parse the value as JSON. + */ +function extractMethodArgs(values: string[]) { + return (param: ParamType, index: number) => { + const value = values[index]; + if (isArrayParameter(param.baseType)) { + return JSON.parse(value); + } + return value; + }; +} + +/** + * Encodes the method and parameters of a contract interaction. + */ +export function encodeMethodAndParams( + abi: string, + method: FunctionFragment, + values: string[] +) { + const contractInterface = new Interface(abi); + const parameterValues = method.inputs.map(extractMethodArgs(values)); + return contractInterface.encodeFunctionData(method, parameterValues); +} + +/** + * Returns the transaction data for an ERC20 transfer. + */ +export function getERC20TokenTransferTransactionData( + recipientAddress: string, + amount: BigNumberish +): string { + const contractInterface = new Interface(ERC20_ABI); + return contractInterface.encodeFunctionData('transfer', [ + recipientAddress, + amount + ]); +} + +/** + * Returns the transaction data for an ERC721 transfer. + */ +export function getERC721TokenTransferTransactionData( + fromAddress: string, + recipientAddress: string, + id: BigNumberish +): string { + const contractInterface = new Interface(ERC721_ABI); + return contractInterface.encodeFunctionData('safeTransferFrom', [ + fromAddress, + recipientAddress, + id + ]); +} diff --git a/src/plugins/oSnap/utils/coins.ts b/src/plugins/oSnap/utils/coins.ts new file mode 100644 index 00000000000..19d6c583a03 --- /dev/null +++ b/src/plugins/oSnap/utils/coins.ts @@ -0,0 +1,61 @@ +import { Network } from '../types'; + +export const ETHEREUM_COIN = { + name: 'Ether', + decimals: 18, + symbol: 'ETH', + logoUri: + 'https://safe-transaction-assets.safe.global/chains/1/currency_logo.png', + address: 'main' +} as const; + +export const MATIC_COIN = { + name: 'MATIC', + decimals: 18, + symbol: 'MATIC', + address: 'main', + logoUri: + 'https://safe-transaction-assets.safe.global/chains/137/currency_logo.png' +} as const; + +const EWC_COIN = { + name: 'Energy Web Token', + symbol: 'EWT', + address: 'main', + decimals: 18, + logoUri: + 'https://safe-transaction-assets.safe.global/chains/246/currency_logo.png' +} as const; + +const XDAI_COIN = { + name: 'XDAI', + symbol: 'XDAI', + address: 'main', + decimals: 18, + logoUri: + 'https://safe-transaction-assets.safe.global/chains/100/currency_logo.png' +} as const; +const BNB_COIN = { + name: 'BNB', + symbol: 'BNB', + address: 'main', + decimals: 18, + logoUri: + 'https://safe-transaction-assets.safe.global/chains/56/currency_logo.png' +} as const; + +export function getNativeAsset(network: Network) { + switch (parseInt(network)) { + case 137: + case 80001: + return MATIC_COIN; + case 100: + return XDAI_COIN; + case 246: + return EWC_COIN; + case 56: + return BNB_COIN; + } + + return ETHEREUM_COIN; +} diff --git a/src/plugins/oSnap/utils/events.ts b/src/plugins/oSnap/utils/events.ts new file mode 100644 index 00000000000..4a195b63be2 --- /dev/null +++ b/src/plugins/oSnap/utils/events.ts @@ -0,0 +1,171 @@ +import { Contract, Event, EventFilter } from '@ethersproject/contracts'; +import bb from 'bluebird'; + +// This state is meant for adjusting a start/end block when querying events. Some apis will fail if the range +// is too big, so the following functions will adjust range dynamically. +export type RangeState = { + startBlock: number; + endBlock: number; + maxRange: number; + currentRange: number; + currentStart: number; // This is the start value you want for your query. + currentEnd: number; // this is the end value you want for your query. + done: boolean; // Signals we successfully queried the entire range. + multiplier?: number; // Multiplier increases or decreases range by this value, depending on success or failure +}; + +/** + * rangeStart. This starts a new range query and sets defaults for state. Use this as the first call before starting your queries + * + * @param {Pick} state + * @returns {RangeState} + */ +export function rangeStart( + state: Pick & { + maxRange?: number; + } +): RangeState { + const { startBlock, endBlock, multiplier = 2 } = state; + if (state.maxRange && state.maxRange > 0) { + const range = endBlock - startBlock; + if (!(range >= 0)) { + throw new Error('End block must be higher than start block'); + } + const currentRange = Math.min(state.maxRange, range); + const currentStart = endBlock - currentRange; + const currentEnd = endBlock; + return { + done: false, + startBlock, + endBlock, + maxRange: state.maxRange, + currentRange, + currentStart, + currentEnd, + multiplier + }; + } else { + // the largest range we can have, since this is the users query for start and end + const maxRange = endBlock - startBlock; + if (!(maxRange > 0)) { + throw new Error('End block must be higher than start block'); + } + const currentStart = startBlock; + const currentEnd = endBlock; + const currentRange = maxRange; + + return { + done: false, + startBlock, + endBlock, + maxRange, + currentRange, + currentStart, + currentEnd, + multiplier + }; + } +} +/** + * rangeSuccessDescending. We have 2 ways of querying events, from oldest to newest, or newest to oldest. Typically we want them in order, from + * oldest to newest, but for this particular case we want them newest to oldest, ie descending ( larger timestamp to smaller timestamp). + * This function will increase the range between start/end block and return a new start/end to use since by calling this you are signalling + * that the last range ended in a successful query. + * + * @param {RangeState} state + * @returns {RangeState} + */ +export function rangeSuccessDescending(state: RangeState): RangeState { + const { + startBlock, + currentStart, + maxRange, + currentRange, + multiplier = 2 + } = state; + // we are done if we succeeded querying where the currentStart matches are initial start block + const done = currentStart <= startBlock; + // increase range up to max range for every successful query + const nextRange = Math.min(Math.ceil(currentRange * multiplier), maxRange); + // move our end point to the previously successful start, ie moving from newest to oldest + const nextEnd = currentStart; + // move our start block to the next range down + const nextStart = Math.max(nextEnd - nextRange, startBlock); + return { + ...state, + currentStart: nextStart, + currentEnd: nextEnd, + currentRange: nextRange, + done + }; +} +/** + * rangeFailureDescending. Like the previous function, this will decrease the range between start/end for your query, because you are signalling + * that the last query failed. It will also keep the end of your range the same, while moving the start range up. This is why + * its considered descending, it will attempt to move from end to start, rather than start to end. + * + * @param {RangeState} state + * @returns {RangeState} + */ +export function rangeFailureDescending(state: RangeState): RangeState { + const { startBlock, currentEnd, currentRange, multiplier = 2 } = state; + const nextRange = Math.floor(currentRange / multiplier); + // this will eventually throw an error if you keep calling this function, which protects us against re-querying a broken api in a loop + if (currentRange <= 0) throw new Error('Current range must be above 0'); + if (!(nextRange > 0)) throw new Error('Range must be above 0'); + // we stay at the same end block + const nextEnd = currentEnd; + // move our start block closer to the end block, shrinking the range + const nextStart = Math.max(nextEnd - nextRange, startBlock); + return { + ...state, + currentStart: nextStart, + currentEnd: nextEnd, + currentRange: nextRange + }; +} + +// The main interface to wrap the above pure functions up. requires you to pass in a generic function +// which returns the events based on a start/end block query. +export async function pageEvents( + startBlock: number, + endBlock: number, + maxRange: number, + //start and end block range to query + fetchEvents: (query: { start: number; end: number }) => Promise, + concurrency: number = 5 +): Promise { + let state = rangeStart({ startBlock, endBlock, maxRange }); + const ranges: { start: number; end: number; index: number }[] = []; + let index = 0; + do { + ranges.push({ + start: state.currentStart, + end: state.currentEnd, + index: index++ + }); + state = rangeSuccessDescending(state); + } while (!state.done); + + return (await bb.map(ranges, fetchEvents, { concurrency })).flat(); +} + +export async function getPagedEvents(params: { + contract: Contract; + eventFilter: EventFilter; + startBlock: number; + latestBlock: number; + maxRange: number; +}) { + const { contract, eventFilter, startBlock, latestBlock, maxRange } = params; + const eventPager = ({ start, end }: { start: number; end: number }) => { + return contract.queryFilter(eventFilter, start, end); + }; + const pagedEvents = await pageEvents( + startBlock, + latestBlock, + maxRange, + eventPager + ); + return pagedEvents as EventType[]; +} diff --git a/src/plugins/oSnap/utils/getters.ts b/src/plugins/oSnap/utils/getters.ts new file mode 100644 index 00000000000..cbbfe337cdb --- /dev/null +++ b/src/plugins/oSnap/utils/getters.ts @@ -0,0 +1,755 @@ +import { TreasuryWallet } from '@/helpers/interfaces'; +import { defaultAbiCoder } from '@ethersproject/abi'; +import { BigNumber } from '@ethersproject/bignumber'; +import { Contract } from '@ethersproject/contracts'; +import { keccak256 } from '@ethersproject/keccak256'; +import { StaticJsonRpcProvider } from '@ethersproject/providers'; +import { pack } from '@ethersproject/solidity'; +import { toUtf8Bytes } from '@ethersproject/strings'; +import { multicall } from '@snapshot-labs/snapshot.js/src/utils'; +import getProvider from '@snapshot-labs/snapshot.js/src/utils/provider'; +import memoize from 'lodash/memoize'; +import { + ERC20_ABI, + GNOSIS_SAFE_TRANSACTION_API_URLS, + OPTIMISTIC_GOVERNOR_ABI, + OPTIMISTIC_ORACLE_V3_ABI, + contractData, + safePrefixes, + solidityZeroHexString +} from '../constants'; +import { + AssertionGql, + AssertionMadeEvent, + BalanceResponse, + CollateralDetails, + NFT, + Network, + OGModuleDetails, + OGProposalState, + OptimisticGovernorTransaction, + ProposalExecutedEvent, + SafeNetworkPrefix, + TransactionsProposedEvent +} from '../types'; +import { getPagedEvents } from './events'; + +/** + * Calls the Gnosis Safe Transaction API + * + * Ideal usage is to specify the shape of the response with the generic type parameter, assuming that the shape of the response is known. + */ +async function callGnosisSafeTransactionApi( + network: Network, + url: string +) { + const apiUrl = GNOSIS_SAFE_TRANSACTION_API_URLS[network]; + const response = await fetch(apiUrl + url); + return response.json() as TResult; +} + +/** + * Fetches the balances of the tokens owned by a given Safe. + */ +export const getGnosisSafeBalances = memoize( + (network: Network, safeAddress: string) => { + const endpointPath = `/v1/safes/${safeAddress}/balances/`; + return callGnosisSafeTransactionApi[]>( + network, + endpointPath + ); + }, + (safeAddress, network) => `${safeAddress}_${network}` +); + +/** + * Fetches the collectibles owned by a given Safe. + */ +export const getGnosisSafeCollectibles = memoize( + (network: Network, safeAddress: string) => { + const endpointPath = `/v2/safes/${safeAddress}/collectibles/`; + // the endpoint returns the data in this form, most likely to allow you to page the data. + type Result = { + count: number; + next: unknown; + previous: unknown; + results: NFT[]; + }; + return callGnosisSafeTransactionApi(network, endpointPath); + }, + (safeAddress, network) => `${safeAddress}_${network}` +); + +/** + * Fetches the block number of a given contract's deployment. + */ +function getDeployBlock(params: { network: Network; name: string }): number { + const results = contractData.filter( + contract => + contract.network === params.network && contract.name === params.name + ); + if (results.length === 1) return results[0].deployBlock ?? 0; + return 0; +} + +/** + * Fetches the subgraph url for a given contract on a given network. + */ +function getContractSubgraph(params: { network: Network; name: string }) { + const results = contractData.filter( + contract => + contract.network === params.network && contract.name === params.name + ); + if (results.length > 1) + throw new Error( + `Too many results finding ${params.name} subgraph on network ${params.network}` + ); + if (results.length < 1) + throw new Error( + `No results finding ${params.name} subgraph on network ${params.network}` + ); + if (!results[0].subgraph) + throw new Error( + `No subgraph url defined for ${params.name} on network ${params.network}` + ); + return results[0].subgraph; +} + +/** + * A helper that wraps the getContractSubgraph function to return the subgraph url for the OptimisticGovernor contract on a given network. + */ +function getOptimisticGovernorSubgraph(network: Network): string { + return getContractSubgraph({ network, name: 'OptimisticGovernor' }); +} + +/** + * A helper that wraps the getContractSubgraph function to return the subgraph url for the OptimisticOracleV3 contract on a given network. + */ +function getOracleV3Subgraph(network: Network): string { + return getContractSubgraph({ network, name: 'OptimisticOracleV3' }); +} + +/** + * Executes a graphql query. + * + * Ideal usage is to specify the shape of the response with the generic type parameter, assuming that the shape of the response is known. + */ +export const queryGql = async (url: string, query: string) => { + try { + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json' + }, + body: JSON.stringify({ query: query }) + }); + if (!response.ok) { + const errorData = await response.json(); + throw new Error( + `Network Error: ${response.status}, message: ${errorData.message}` + ); + } + const data = await response.json(); + // Throw an error if there are errors in the GraphQL response + if (data.errors) { + throw new Error( + `GraphQL Error: ${data.errors.map(error => error.message).join(', ')}` + ); + } + return data.data as Result; + } catch (error) { + throw new Error( + `Network error: ${error instanceof Error ? error.message : error}` + ); + } +}; + +/** + * Returns the address of the Optimistic Governor contract deployment associated with a given treasury (Safe) from the graph. + */ +export const getModuleAddressForTreasury = async ( + network: Network, + treasuryAddress: string +) => { + const subgraph = getOptimisticGovernorSubgraph(network); + const query = ` + query getModuleAddressForTreasury { + safe(id: "${treasuryAddress.toLowerCase()}") { + optimisticGovernor { + id + } + } + } + `; + + type Result = { + safe: { optimisticGovernor: { id: string } }; + }; + + const result = await queryGql(subgraph, query); + return result?.safe?.optimisticGovernor?.id ?? ''; +}; + +/** + * Checks if a given treasury (Safe) has enabled oSnap. + */ +export const getIsOsnapEnabled = async ( + network: Network, + safeAddress: string +) => { + const subgraph = getOptimisticGovernorSubgraph(network); + const query = ` + query isOSnapEnabled { + safe(id:"${safeAddress.toLowerCase()}"){ + isOptimisticGovernorEnabled + } + } + `; + type Result = { + safe: { isOptimisticGovernorEnabled: boolean }; + }; + const result = await queryGql(subgraph, query); + return result?.safe?.isOptimisticGovernorEnabled ?? false; +}; + +/** + * Takes an array of treasuries and checks if any of them have oSnap enabled. + */ +export const getSpaceHasOsnapEnabledTreasury = async ( + treasuries: TreasuryWallet[] +) => { + const isOsnapEnabledOnTreasuriesResult = await Promise.all( + treasuries.map(treasury => + getIsOsnapEnabled(treasury.network as Network, treasury.address) + ) + ); + return isOsnapEnabledOnTreasuriesResult.some( + isOsnapEnabled => isOsnapEnabled + ); +}; + +/** + * Creates the url for the Safe app to configure oSnap. + * + * The data that the Safe app needs is encoded as URL search params. + */ +export function makeConfigureOsnapUrl(params: { + safeAddress: string; + network: Network; + spaceName: string; + spaceUrl: string; + baseUrl?: string; + appUrl?: string; +}) { + const { + safeAddress, + network, + spaceName, + spaceUrl, + baseUrl = 'https://app.safe.global/apps/open', + appUrl = 'https://osnap.uma.xyz/' + } = params; + const safeAddressPrefix = getSafeNetworkPrefix(network); + const appUrlSearchParams = new URLSearchParams(); + appUrlSearchParams.set('spaceName', spaceName); + appUrlSearchParams.set('spaceUrl', spaceUrl); + const appUrlSearch = appUrlSearchParams.toString(); + const safeAppSearchParams = new URLSearchParams(); + safeAppSearchParams.set('safe', `${safeAddressPrefix}:${safeAddress}`); + safeAppSearchParams.set('appUrl', `${appUrl}?${appUrlSearch}`); + const safeAppSearch = safeAppSearchParams.toString(); + const url = `${baseUrl}?${safeAppSearch}`; + return url; +} + +/** + * Fetches the details of a given assertion from the Optimistic Oracle V3 subgraph. + */ +async function getAssertionGql(params: { + network: Network; + assertionId: string; +}) { + const { assertionId, network } = params; + const oracleUrl = getOracleV3Subgraph(network); + const request = ` + { + assertion(id:"${assertionId}"){ + assertionId + expirationTime + assertionHash + disputeHash + settlementHash + assertionLogIndex + settlementResolution + } + } + `; + type Result = { + assertion: AssertionGql | undefined; + }; + const result = await queryGql(oracleUrl, request); + return result?.assertion; +} +/** + * Fetches the details of a given proposal from the Optimistic Governor subgraph. + * + * The subgraph uses the `assertionId` that comes from assertion events as the primary key for proposals. + * However, this `assertionId` will be deleted if the proposal is disputed, so we can't use it to query the subgraph. + * Instead, we use the `proposalHash` and `explanation` to query the subgraph. + * The `explanation` contains the ipfs url of the proposal, which is the only way to distinguish between proposals with the same `proposalHash`. + * This means we must use a `where` clause to filter the results, which is not ideal. + */ +async function getOgProposalGql(params: { + network: Network; + explanation: string; + moduleAddress: string; + proposalHash: string; +}) { + const { network, explanation, moduleAddress, proposalHash } = params; + const encodedExplanation = pack( + ['bytes'], + [toUtf8Bytes(explanation.replace(/^0x/, ''))] + ); + const subgraph = getOptimisticGovernorSubgraph(network); + const request = ` +{ + proposals(where:{proposalHash:"${proposalHash}",explanation:"${encodedExplanation}",optimisticGovernor:"${moduleAddress.toLowerCase()}"}){ + id + executed + deleted + assertionId + } +} +`; + type Result = { + proposals: { + id: string; + executed: boolean; + assertionId: string; + deleted: boolean; + }[]; + }; + const result = await queryGql(subgraph, request); + // we can only use the gql `where` clause when querying a list, but we know that there will only be one result. + return result?.proposals[0]; +} + +/** + * Fetches the details of a Optimistic Governor module's collateral token. + * + * Returns the address, symbol, and decimals of the collateral token, along with the token contract for further querying. + */ +export async function getCollateralDetailsForProposal( + provider: StaticJsonRpcProvider, + moduleAddress: string +): Promise { + const moduleContract = new Contract( + moduleAddress, + OPTIMISTIC_GOVERNOR_ABI, + provider + ); + + const erc20Contract = new Contract( + await moduleContract.collateral(), + ERC20_ABI, + provider + ); + + const address = erc20Contract.address; + const symbol: string = await erc20Contract.symbol(); + const decimals: BigNumber = await erc20Contract.decimals(); + + return { erc20Contract, address, symbol, decimals }; +} + +/** + * Fetches the allowance of a given collateral token for a given user. + */ +export async function getUserCollateralAllowance( + erc20Contract: Contract, + userAddress: string, + moduleAddress: string +) { + return erc20Contract.allowance(userAddress, moduleAddress); +} + +/** + * Fetches the balance of a given collateral token for a given user. + */ +export async function getUserCollateralBalance( + erc20Contract: Contract, + userAddress: string +) { + return erc20Contract.balanceOf(userAddress); +} + +/** + * Fetches the details of a given Optimistic Governor module from the chain. + * + * Performs a multicall to fetch the oracle address, rules, minimum bond, and challenge period. + */ +export async function getOGModuleDetails(params: { + provider: StaticJsonRpcProvider; + network: Network; + moduleAddress: string; + transactions: OptimisticGovernorTransaction[]; +}): Promise { + const { provider, network, moduleAddress } = params; + const moduleDetails: [ + [oracleAddress: string], + [rules: string], + [minimumBond: BigNumber], + [challengePeriod: BigNumber] + ] = await multicall(network, provider, OPTIMISTIC_GOVERNOR_ABI as any, [ + [moduleAddress, 'optimisticOracleV3'], + [moduleAddress, 'rules'], + [moduleAddress, 'bondAmount'], + [moduleAddress, 'liveness'] + ]); + + const oracleAddress = moduleDetails[0][0]; + const rules = moduleDetails[1][0]; + const minimumBond = moduleDetails[2][0]; + const challengePeriod = moduleDetails[3][0]; + + return { + moduleAddress, + oracleAddress, + rules, + minimumBond, + challengePeriod + }; +} + +/** + * Fetches the state of an Optimistic Governor proposal from the chain. + * + * This is a fallback function that should only be used if the subgraph is not available, because it is very slow. + * + * The contract is designed in such a way that it deletes the `assertionId` from the proposal if the proposal is disputed, _or_ if the transactions are executed successfully. This means we can't tell the difference between a proposal that has not yet been proposed, has been disputed, or that has been executed by querying the chain. + * + * Instead, we must query the chain for the proposal events, and then query the chain for the execution events, and then compare the two to determine the state of the proposal. This is very slow. + */ +export async function getOgProposalStateFromChain(params: { + moduleDetails: OGModuleDetails; + network: Network; + proposalHash: string; + explanation: string; +}): Promise { + const { network, moduleDetails, explanation, proposalHash } = params; + const { moduleAddress, oracleAddress } = moduleDetails; + + const provider = getProvider(network); + const latestBlock = (await provider.getBlock('latest')).number; + const oGstartBlock = getDeployBlock({ network, name: 'OptimisticGovernor' }); + const oOStartBlock = getDeployBlock({ network, name: 'OptimisticOracleV3' }); + const maxRange = 3000; + + const moduleContract = new Contract( + moduleAddress, + OPTIMISTIC_GOVERNOR_ABI, + provider + ); + const oracleContract = new Contract( + oracleAddress, + OPTIMISTIC_ORACLE_V3_ABI, + provider + ); + + const assertionId: string = await moduleContract.assertionIds(proposalHash); + const hasAssertionId = assertionIdIsNotZero(assertionId); + + if (hasAssertionId) { + const assertionMadeEventForCurrentAssertionIdFilter = + oracleContract.filters.AssertionMade(assertionId); + + const assertionMadeEvents = await getPagedEvents({ + contract: oracleContract, + eventFilter: assertionMadeEventForCurrentAssertionIdFilter, + startBlock: oOStartBlock, + latestBlock, + maxRange + }); + + // assertion ids are unique, so this will have only one result + // we need to get an event instead of getting the `Assertion` struct from the chain because the oracle dapp needs the assertion transaction hash and the log index to link to the oracle dapp. + const assertionMadeEvent = assertionMadeEvents[0]; + + const expirationTime = assertionMadeEvent.args?.expirationTime.toNumber(); + const isInChallengePeriod = expirationTime * 1000 > Date.now(); + const assertionHash = assertionMadeEvent.transactionHash; + const assertionLogIndex = assertionMadeEvent.logIndex.toString(); + + if (isInChallengePeriod) { + return { + status: 'in-oo-challenge-period', + assertionHash, + assertionLogIndex, + expirationTime + }; + } + + return { + status: 'can-request-tx-execution', + assertionHash, + assertionLogIndex + }; + } + + const transactionsProposedEventFilter = + moduleContract.filters.TransactionsProposed(); + + const transactionsProposedEvents = + await getPagedEvents({ + contract: moduleContract, + eventFilter: transactionsProposedEventFilter, + startBlock: oGstartBlock, + latestBlock, + maxRange + }); + + const transactionsProposedEventsThatMatch = transactionsProposedEvents.filter( + event => + event.args?.proposalHash === proposalHash && + event.args?.explanation === explanation + ); + + const hasTransactionProposedEvents = + transactionsProposedEventsThatMatch.length > 0; + + // we can return early and skip querying for execution events if there are no proposal events + if (!hasTransactionProposedEvents) { + return { + status: 'can-propose-to-og', + isDisputed: false + }; + } + + // the proposal hash is not indexed on the transactions proposed event unfortunately, but it is on the proposal executed event. + // so at least we can use it for this one to narrow down the results. + const proposalExecutedEventFilter = + moduleContract.filters.ProposalExecuted(proposalHash); + + const proposalExecutedEvents = await getPagedEvents({ + contract: moduleContract, + eventFilter: proposalExecutedEventFilter, + startBlock: oGstartBlock, + latestBlock, + maxRange + }); + + // we know that the transactions have been executed if there is an execution event with an assertion id that matches an assertion id for a proposal event + let proposalExecuted = false; + for (const proposalExecutedEvent of proposalExecutedEvents) { + for (const transactionsProposedEvent of transactionsProposedEventsThatMatch) { + if ( + proposalExecutedEvent.args?.assertionId === + transactionsProposedEvent.args?.assertionId + ) { + proposalExecuted = true; + break; + } + } + } + + if (proposalExecuted) { + const assertionMadeEventForExecutedAssertionIdFilter = + oracleContract.filters.AssertionMade(assertionId); + + const assertionMadeEvents = await getPagedEvents({ + contract: oracleContract, + eventFilter: assertionMadeEventForExecutedAssertionIdFilter, + startBlock: oOStartBlock, + latestBlock, + maxRange + }); + + // assertion ids are unique, so this will have only one result + const assertionMadeEvent = assertionMadeEvents[0]; + + return { + status: 'transactions-executed', + assertionHash: assertionMadeEvent.transactionHash, + assertionLogIndex: assertionMadeEvent.logIndex.toString() + }; + } + + return { + status: 'can-propose-to-og', + isDisputed: false + }; +} + +/** + * Fetches the state of an Optimistic Governor proposal from the subgraph. + * + * This is the preferred method of fetching the state of a proposal, because it is much faster than querying the chain. + */ +export async function getOGProposalStateGql(params: { + network: Network; + moduleAddress: string; + proposalHash: string; + explanation: string; +}): Promise { + const { network } = params; + const oGproposal = await getOgProposalGql(params); + + if (!oGproposal) { + return { status: 'can-propose-to-og', isDisputed: false }; + } + + const { executed, assertionId, deleted } = oGproposal; + + const hasAssertionId = assertionIdIsNotZero(assertionId); + + // the subgraph records `ProposalDeleted` events, which are fired when a proposal is disputed. + if (!hasAssertionId && deleted) { + return { status: 'can-propose-to-og', isDisputed: true }; + } + + if (!hasAssertionId) { + return { status: 'can-propose-to-og', isDisputed: false }; + } + + const assertion = await getAssertionGql({ network, assertionId }); + + if (!assertion) { + return { status: 'can-propose-to-og', isDisputed: false }; + } + + const { + assertionHash, + settlementHash, + assertionLogIndex, + settlementResolution + } = assertion; + + // if the assertion is settled and the graph says the transactions have been executed, then we know the assertion passed and we can return early + if (executed && settlementHash) { + return { + status: 'transactions-executed', + assertionHash, + assertionLogIndex + }; + } + + // if the settlement hash exists and the settlement resolution is true, then we know that the assertion passed. + // we already checked if the transactions were executed, so we know that the assertion passed but the transactions were not executed yet. + if (settlementHash && settlementResolution) { + return { + status: 'can-request-tx-execution', + assertionHash, + assertionLogIndex + }; + } + + const expirationTime = Number(assertion.expirationTime); + const isExpired = Math.floor(Date.now() / 1000) >= expirationTime; + + // from the above checks, we know that the transactions have not been executed, the assertion has not been disputed, and the assertion has not been settled. + // if the assertion challenge period has not yet expired, then we know we are still in the challenge period. + if (!isExpired) { + return { + status: 'in-oo-challenge-period', + assertionHash, + assertionLogIndex, + expirationTime + }; + } + + // if all the above fails, fall back to the no assertion state + // this should not be possible though + return { status: 'can-propose-to-og', isDisputed: false }; +} + +/** + * Querying for an assertion ID that does not map to a proposal hash will return '0x0000000000000000000000000000000000000000000000000000000000000000' + */ +function assertionIdIsNotZero(assertionId: string) { + return assertionId !== solidityZeroHexString; +} + +/** + * Fetches the state of an Optimistic Governor proposal. + * + * This function will attempt to fetch the state of a proposal from the subgraph, and if that fails, it will fall back to querying the chain. + */ +export async function getOGProposalState(params: { + moduleDetails: OGModuleDetails; + network: Network; + explanation: string; + transactions: OptimisticGovernorTransaction[]; +}): Promise { + const { network, moduleDetails, explanation, transactions } = params; + const { moduleAddress } = moduleDetails; + const proposalHash = getProposalHashFromTransactions(transactions); + try { + return await getOGProposalStateGql({ + network, + moduleAddress, + explanation, + proposalHash + }); + } catch (error) { + console.warn( + 'Error fetching OG proposal state from subgraph, falling back to chain', + error + ); + return getOgProposalStateFromChain({ + network, + moduleDetails, + explanation, + proposalHash + }); + } +} + +/** + * The `proposalHash` as represented in the Optimistic Governor contract is the keccak256 hash of the transactions that make up the proposal. + */ +export function getProposalHashFromTransactions( + transactions: OptimisticGovernorTransaction[] +) { + return keccak256( + defaultAbiCoder.encode( + ['(address to, uint8 operation, uint256 value, bytes data)[]'], + [transactions] + ) + ); +} + +/** + * Returns the EIP-3770 prefix for a given network. + * + * @see SafeNetworkPrefix + */ +export function getSafeNetworkPrefix(network: Network): SafeNetworkPrefix { + return safePrefixes[network]; +} + +/** + * Returns the url for a given Safe app on a given network. + */ +export function getSafeAppLink( + network: Network, + safeAddress: string, + appUrl = 'https://gnosis-safe.io/app/' +) { + const prefix = getSafeNetworkPrefix(network); + return `${appUrl}${prefix}:${safeAddress}`; +} + +/** + * Returns the url for an Optimistic Governor proposal's assertion on the Optimistic Oracle dapp. + */ +export function getOracleUiLink( + chain: string, + txHash: string, + logIndex: number +) { + if (Number(chain) !== 5 && Number(chain) !== 80001) { + return `https://oracle.uma.xyz?transactionHash=${txHash}&eventIndex=${logIndex}`; + } + return `https://testnet.oracle.uma.xyz?transactionHash=${txHash}&eventIndex=${logIndex}`; +} diff --git a/src/plugins/oSnap/utils/index.ts b/src/plugins/oSnap/utils/index.ts new file mode 100644 index 00000000000..b903e6f3aea --- /dev/null +++ b/src/plugins/oSnap/utils/index.ts @@ -0,0 +1,7 @@ +export * from './abi'; +export * from './coins'; +export * from './events'; +export * from './getters'; +export * from './proposal'; +export * from './transactions'; +export * from './validators'; diff --git a/src/plugins/oSnap/utils/proposal.ts b/src/plugins/oSnap/utils/proposal.ts new file mode 100644 index 00000000000..e0815faa6d9 --- /dev/null +++ b/src/plugins/oSnap/utils/proposal.ts @@ -0,0 +1,75 @@ +import { BigNumber } from '@ethersproject/bignumber'; +import { toUtf8Bytes } from '@ethersproject/strings'; +import { sendTransaction } from '@snapshot-labs/snapshot.js/src/utils'; +import { ERC20_ABI, OPTIMISTIC_GOVERNOR_ABI } from '../constants'; +import { OptimisticGovernorTransaction } from '../types'; + +/** + * The user must approve the spend of the collateral token before they can submit a proposal. + * + * If the proposal is disputed and fails a vote, the user will lose their bond. + */ +export async function* approveBond( + web3: any, + moduleAddress: string, + collateralAddress: string, + minimumBond: BigNumber +) { + const approveTx = await sendTransaction( + web3, + collateralAddress, + ERC20_ABI as any, + 'approve', + [moduleAddress, minimumBond], + {} + ); + yield approveTx; + const approvalReceipt = await approveTx.wait(); + console.log('[DAO module] token transfer approved:', approvalReceipt); + yield; +} + +/** + * Submits a proposal to the Optimistic Governor. + */ +export async function* submitProposal( + web3: any, + moduleAddress: string, + explanation: string, + transactions: OptimisticGovernorTransaction[] +) { + const explanationBytes = toUtf8Bytes(explanation); + const tx = await sendTransaction( + web3, + moduleAddress, + OPTIMISTIC_GOVERNOR_ABI as any, + 'proposeTransactions', + [transactions, explanationBytes] + // [[["0xB8034521BB1a343D556e5005680B3F17FFc74BeD", 0, "0", "0x"]], '0x'] + ); + yield tx; + const receipt = await tx.wait(); + console.log('[DAO module] submitted proposal:', receipt); +} + +/** + * Executes a proposal on the Optimistic Governor. + * + * This can only be done after the dispute window has ended. + */ +export async function* executeProposal( + web3: any, + moduleAddress: string, + transactions: OptimisticGovernorTransaction[] +) { + const tx = await sendTransaction( + web3, + moduleAddress, + OPTIMISTIC_GOVERNOR_ABI as any, + 'executeProposal', + [transactions] + ); + yield tx; + const receipt = await tx.wait(); + console.log('[DAO module] executed proposal:', receipt); +} diff --git a/src/plugins/oSnap/utils/transactions.ts b/src/plugins/oSnap/utils/transactions.ts new file mode 100644 index 00000000000..00eaec9eea7 --- /dev/null +++ b/src/plugins/oSnap/utils/transactions.ts @@ -0,0 +1,157 @@ +import { FunctionFragment } from '@ethersproject/abi'; +import { BigNumber } from '@ethersproject/bignumber'; +import { + ContractInteractionTransaction, + NFT, + OptimisticGovernorTransaction, + RawTransaction, + Token, + TransferFundsTransaction, + TransferNftTransaction +} from '../types'; +import { encodeMethodAndParams } from './abi'; + +/** + * Creates a formatted transaction for the Optimistic Governor to execute + * + * note: the value for `operation` is always zero because we do not support delegatecall. + * + * @see OptimisticGovernorTransaction + */ +export function createFormattedOptimisticGovernorTransaction({ + to, + value, + data +}: { + to: string; + value: string; + data: string; +}): OptimisticGovernorTransaction { + return [to, 0, value, data]; +} + +/** + * Creates a raw transaction for the Optimistic Governor to execute + * + * @see RawTransaction + */ +export function createRawTransaction(params: { + to: string; + value: string; + data: string; +}): RawTransaction { + const type = 'raw'; + const formatted = createFormattedOptimisticGovernorTransaction(params); + return { + ...params, + type, + formatted + }; +} + +/** + * Creates a transaction to transfer an NFT + * + * @see TransferNftTransaction + */ +export function createTransferNftTransaction(params: { + recipient: string; + collectable: NFT; + data: string; +}): TransferNftTransaction { + const type = 'transferNFT'; + const to = params.collectable.address; + const value = '0'; + const data = params.data; + const formatted = createFormattedOptimisticGovernorTransaction({ + to, + value, + data + }); + + return { + ...params, + type, + to, + value, + data, + formatted + }; +} + +/** + * Creates a transaction to transfer funds + * + * @see TransferFundsTransaction + */ +export function createTransferFundsTransaction(params: { + recipient: string; + amount: string; + token: Token; + data: string; +}): TransferFundsTransaction { + const type = 'transferFunds'; + const isNativeToken = params.token.address === 'main'; + const data = isNativeToken ? '0x' : params.data; + const to = isNativeToken ? params.recipient : params.token.address; + const amount = parseAmount(params.amount); + const value = isNativeToken ? amount : '0'; + const formatted = createFormattedOptimisticGovernorTransaction({ + to, + value, + data + }); + return { + ...params, + type, + data, + to, + value, + amount, + formatted + }; +} + +/** + * Creates a transaction to interact with a contract. + * + * the `method` is executed with the given `parameters`. + * + * @see ContractInteractionTransaction + */ +export function createContractInteractionTransaction(params: { + to: string; + value: string; + abi: string; + method: FunctionFragment; + parameters: string[]; +}): ContractInteractionTransaction { + const type = 'contractInteraction'; + const data = encodeMethodAndParams( + params.abi, + params.method, + params.parameters + ); + const formatted = createFormattedOptimisticGovernorTransaction({ + ...params, + data + }); + return { + ...params, + data, + type, + formatted + }; +} + +export function parseAmount(input: string) { + return BigNumber.from(input).toString(); +} + +export function parseValueInput(input: string) { + try { + return parseAmount(input); + } catch (e) { + return input; + } +} diff --git a/src/plugins/oSnap/utils/validators.ts b/src/plugins/oSnap/utils/validators.ts new file mode 100644 index 00000000000..5c1ecacc5e1 --- /dev/null +++ b/src/plugins/oSnap/utils/validators.ts @@ -0,0 +1,70 @@ +import { isAddress } from '@ethersproject/address'; +import { + JsonRpcProvider, + StaticJsonRpcProvider +} from '@ethersproject/providers'; +import memoize from 'lodash/memoize'; +import { Contract } from '@ethersproject/contracts'; +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 } from '../types'; + +/** + * Validates that the given `address` is a valid Ethereum address + */ +export const mustBeEthereumAddress = memoize((address: string) => { + const startsWith0x = address?.startsWith('0x'); + const isValidAddress = isAddress(address); + return startsWith0x && isValidAddress; +}); + +/** + * Validates that the given `address` is a valid Ethereum contract address + */ +export const mustBeEthereumContractAddress = memoize( + async (network: string, address: string) => { + const provider = getProvider(network) as JsonRpcProvider; + const contractCode = await provider.getCode(address); + + return ( + contractCode && contractCode.replace(/^0x/, '').replace(/0/g, '') !== '' + ); + }, + (url, contractAddress) => `${url}_${contractAddress}` +); + +/** + * Validates a transaction. + */ +export function validateTransaction(transaction: BaseTransaction) { + const addressEmptyOrValidate = + transaction.to === '' || isAddress(transaction.to); + return ( + isBigNumberish(transaction.value) && + addressEmptyOrValidate && + (!transaction.data || isHexString(transaction.data)) + ); +} + +/** + * Validates a module address. + */ +export async function validateModuleAddress( + network: string, + moduleAddress: string +): Promise { + if (!isAddress(moduleAddress)) return false; + const provider: StaticJsonRpcProvider = getProvider(network); + const moduleContract = new Contract( + moduleAddress, + OPTIMISTIC_GOVERNOR_ABI, + provider + ); + + return moduleContract + .rules() + .then(() => true) + .catch(() => false); +} diff --git a/src/views/SpaceCreate.vue b/src/views/SpaceCreate.vue index 00c1d6f3f00..e5dea1b6b7f 100644 --- a/src/views/SpaceCreate.vue +++ b/src/views/SpaceCreate.vue @@ -1,10 +1,10 @@