-
Notifications
You must be signed in to change notification settings - Fork 97
feat: migrate governance scripts from deployment-scripts branch #853
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 23 commits
784b473
d458340
2ce452e
b2856fc
4fd8134
f3a5109
074bd5b
21930b6
3f4eac9
ca2d12a
9e273fc
e877da1
2997b16
cbfeff5
69c2af9
22bfeaa
fe0aa64
797559b
2c9b54d
5a1fcca
d808335
a049830
99f6787
702cb47
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| WALLET_KEY= | ||
| LEDGER_BIP32_PATH= | ||
| GUARDIAN_KEY= | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| output | ||
| contract-bindings | ||
| .env | ||
| .env.testnet | ||
| .env.mainnet | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| [ | ||
| { | ||
| "chainId": 10002, | ||
| "vaa": "AQAAAAABAO9GFw0p7M1k1+dApnFYahEtKd6We4dPSZa+FirgR0BNWAcESgcQvNguSV66noi7/0dY+/Vg4NvyjC3WSPsiTBcAAAAAALXylaUAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAExxsVgsLk1ikgAAAAAAAAAABHZW5lcmFsUHVycG9zZUdvdmVybmFuY2UBJxKVF/AWTB0ImtcuZp5XuQiHkJZtvXOBQe/2WWJfLq1P7s382UFVxn8YAATO/BQp" | ||
| }, | ||
| { | ||
| "chainId": 10003, | ||
| "vaa": "AQAAAAABABRt1qnWaWa2nRm22+NDCbmX1SIt6uck4CFZf5OFhrHVP8HhCMWOfobvSmnrAp0aBCyg1Wq/smyZybczTthXnmMAAAAAAL931RgAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAE0TeyxhuLDpwgAAAAAAAAAABHZW5lcmFsUHVycG9zZUdvdmVybmFuY2UBJxOBtlpI3KzLoErKPAVcQRKwcVuQwDldPHQjLRKRbsqJUro1K00ngYA1AATO/BQp" | ||
| }, | ||
| { | ||
| "chainId": 10004, | ||
| "vaa": "AQAAAAABAH345FjR31wRQxliMQKFNmGmerZZ3VCzfJxnW4LYABHPCGlnicrUHmXSXfx6hMvdr560prlSoJqO8ard2a+dhJwAAAAAAEB58FMAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAE19jKl8J3pksgAAAAAAAAAABHZW5lcmFsUHVycG9zZUdvdmVybmFuY2UBJxRyClkSi5btpuwpQMeJlAbk3FbQ3B0w54t8f7vO+Hrm6XtTibLkcMpKAATO/BQp" | ||
| }, | ||
| { | ||
| "chainId": 10005, | ||
| "vaa": "AQAAAAABAGHthpHgkBKYghkiZ9wlsjq/yzfOrqtNQI84ZmrQcQX+NFQG2o1XhL2Gm+/iYZjFpmAX3Z9vVs+iMjstsbNwpZsBAAAAAK7WazAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEMpcRWPILd/UgAAAAAAAAAABHZW5lcmFsUHVycG9zZUdvdmVybmFuY2UBJxXOHeHqSwQNMkoHcZBDpiNMlP0LXd3+q8zyBjzWb1OhIY4jxoG6bnliAATO/BQp" | ||
| } | ||
| ] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| { | ||
| "name": "ts-scripts", | ||
| "version": "1.0.0", | ||
| "private": true, | ||
| "description": "", | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| "main": "index.js", | ||
| "scripts": { | ||
| "build": "npx typechain --target=ethers-v5 --out-dir=./contract-bindings ../out/*/*.json" | ||
| }, | ||
| "author": "", | ||
| "license": "ISC", | ||
| "devDependencies": { | ||
| "@certusone/wormhole-sdk": "^0.10.11", | ||
| "@typechain/ethers-v5": "^11.1.2", | ||
| "@types/node": "^20.11.22", | ||
| "@xlabs-xyz/ledger-signer": "^0.0.3", | ||
| "dotenv": "^16.4.5", | ||
| "ethers": "^5.7.2", | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It might be worth bumping this to To ensure no other dependency pulls in the old version too.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. great suggestion, thank you. |
||
| "typechain": "^8.3.2", | ||
| "tsx": "^4.7.2", | ||
| "typescript": "^5.4.5" | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,214 @@ | ||
| import { ChainId } from "@certusone/wormhole-sdk"; | ||
| import { ethers } from "ethers"; | ||
| import fs from "fs"; | ||
|
|
||
| export type ChainInfo = { | ||
| evmNetworkId: number; | ||
| chainId: ChainId; | ||
| rpc: string; | ||
| wormholeAddress: string; | ||
| }; | ||
|
|
||
| export type Deployment = { | ||
| chainId: ChainId; | ||
| address: string; | ||
| }; | ||
|
|
||
| export type ContractsJson = { | ||
| GeneralPurposeGovernances: Deployment[]; | ||
| }; | ||
|
|
||
| const DEFAULT_ENV = "testnet"; | ||
|
|
||
| export let env = ""; | ||
| let lastRunOverride: boolean | undefined; | ||
|
|
||
| export function init(overrides: { lastRunOverride?: boolean } = {}): string { | ||
| env = get_env_var("ENV"); | ||
| if (!env) { | ||
| console.log( | ||
| "No environment was specified, using default environment files" | ||
| ); | ||
| env = DEFAULT_ENV; | ||
| } | ||
| lastRunOverride = overrides?.lastRunOverride; | ||
|
|
||
| require("dotenv").config({ | ||
| path: `./ts-scripts/.env${env != DEFAULT_ENV ? "." + env : ""}`, | ||
| }); | ||
| return env; | ||
| } | ||
|
|
||
| function get_env_var(env: string): string { | ||
| const v = process.env[env]; | ||
| return v || ""; | ||
| } | ||
|
|
||
| export function loadScriptConfig(filename: string): any { | ||
| const configFile = fs.readFileSync( | ||
| `./ts-scripts/config/${env}/${filename}.json` | ||
| ); | ||
| const config = JSON.parse(configFile.toString()); | ||
| if (!config) { | ||
| throw Error("Failed to pull config file!"); | ||
| } | ||
| return config; | ||
| } | ||
|
|
||
| type ChainConfig = { | ||
| chainId: ChainId; | ||
| }; | ||
|
|
||
| export async function getChainConfig<T extends ChainConfig>( | ||
| filename: string, | ||
| chainId: ChainId | ||
| ): Promise<T> { | ||
| const scriptConfig: T[] = await loadScriptConfig(filename); | ||
|
|
||
| const chainConfig = scriptConfig.find((x) => x.chainId == chainId); | ||
|
|
||
| if (!chainConfig) { | ||
| throw Error(`Failed to find chain config for chain ${chainId}`); | ||
| } | ||
|
|
||
| return chainConfig; | ||
| } | ||
|
|
||
| export function loadOperatingChains(): ChainInfo[] { | ||
| const allChains = loadChains(); | ||
| let operatingChains: number[] | null = null; | ||
|
|
||
| const chainFile = fs.readFileSync(`./ts-scripts/config/${env}/chains.json`); | ||
| const chains = JSON.parse(chainFile.toString()); | ||
| if (chains.operatingChains) { | ||
| operatingChains = chains.operatingChains; | ||
| } | ||
| if (!operatingChains) { | ||
| return allChains; | ||
| } | ||
|
|
||
| const output: ChainInfo[] = []; | ||
| operatingChains.forEach((x: number) => { | ||
| const item = allChains.find((y) => { | ||
| return x == y.chainId; | ||
| }); | ||
| if (item) { | ||
| output.push(item); | ||
| } | ||
| }); | ||
|
|
||
| return output; | ||
| } | ||
|
|
||
| export function loadChains(): ChainInfo[] { | ||
| const chainFile = fs.readFileSync(`./ts-scripts/config/${env}/chains.json`); | ||
| const chains = JSON.parse(chainFile.toString()); | ||
| if (!chains.chains) { | ||
| throw Error("Failed to pull chain config file!"); | ||
| } | ||
| return chains.chains; | ||
| } | ||
|
|
||
| export function getChain(chain: ChainId): ChainInfo { | ||
| const chains = loadChains(); | ||
| const output = chains.find((x) => x.chainId == chain); | ||
| if (!output) { | ||
| throw Error("bad chain ID"); | ||
| } | ||
|
|
||
| return output; | ||
| } | ||
|
|
||
| export function loadPrivateKey(): string { | ||
| const privateKey = get_env_var("WALLET_KEY"); | ||
| if (!privateKey) { | ||
| throw Error("Failed to find private key for this process!"); | ||
| } | ||
| return privateKey; | ||
| } | ||
|
|
||
| export function writeOutputFiles(output: any, processName: string) { | ||
| fs.mkdirSync(`./ts-scripts/output/${env}/${processName}`, { | ||
| recursive: true, | ||
| }); | ||
| fs.writeFileSync( | ||
| `./ts-scripts/output/${env}/${processName}/lastrun.json`, | ||
| JSON.stringify(output), | ||
| { flag: "w" } | ||
| ); | ||
| fs.writeFileSync( | ||
| `./ts-scripts/output/${env}/${processName}/${Date.now()}.json`, | ||
| JSON.stringify(output), | ||
| { flag: "w" } | ||
| ); | ||
| } | ||
|
|
||
| export async function getSigner(chain: ChainInfo): Promise<ethers.Signer> { | ||
| const provider = getProvider(chain); | ||
| const privateKey = loadPrivateKey(); | ||
|
|
||
| if (privateKey === "ledger") { | ||
| if (process.env.LEDGER_BIP32_PATH === undefined) { | ||
| throw new Error(`Missing BIP32 derivation path. | ||
| With ledger devices the path needs to be specified in env var 'LEDGER_BIP32_PATH'.`); | ||
| } | ||
| const { LedgerSigner } = await import("@xlabs-xyz/ledger-signer"); | ||
| return LedgerSigner.create(provider, process.env.LEDGER_BIP32_PATH); | ||
| } | ||
|
|
||
| const signer = new ethers.Wallet(privateKey, provider); | ||
| return signer; | ||
| } | ||
|
|
||
| export function getProvider( | ||
| chain: ChainInfo | ||
| ): ethers.providers.StaticJsonRpcProvider { | ||
| const providerRpc = | ||
| loadChains().find((x: any) => x.chainId == chain.chainId)?.rpc || ""; | ||
|
|
||
| if (!providerRpc) { | ||
| throw new Error("Failed to find a provider RPC for chain " + chain.chainId); | ||
| } | ||
|
|
||
| return new ethers.providers.StaticJsonRpcProvider(providerRpc); | ||
| } | ||
|
|
||
| let contracts: ContractsJson; | ||
| export function loadContracts() { | ||
| if (contracts) { | ||
| return contracts; | ||
| } | ||
|
|
||
| const contractsFile = fs.readFileSync( | ||
| `./ts-scripts/config/${env}/contracts.json` | ||
| ); | ||
|
|
||
| if (!contractsFile) { | ||
| throw Error("Failed to find contracts file for this process!"); | ||
| } | ||
|
|
||
| // NOTE: We assume that the contracts.json file is correctly formed... | ||
| contracts = JSON.parse(contractsFile.toString()) as ContractsJson; | ||
|
|
||
| return loadContracts(); | ||
| } | ||
| type ContractTypes = keyof ContractsJson; | ||
|
|
||
| export async function getAllContracts(contractName: ContractTypes) { | ||
| return (await loadContracts())[contractName]; | ||
| } | ||
|
|
||
| export async function getContractAddress( | ||
| contractName: ContractTypes, | ||
| chainId: ChainId | ||
| ): Promise<string> { | ||
| const contract = (await getAllContracts(contractName))?.find( | ||
| (c) => c.chainId === chainId | ||
| )?.address; | ||
|
|
||
| if (!contract) { | ||
| throw new Error(`No ${contractName} contract found for chain ${chainId}`); | ||
| } | ||
|
|
||
| return contract; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,88 @@ | ||
| import { inspect } from "util"; | ||
| import { ChainId } from "@certusone/wormhole-sdk"; | ||
|
|
||
| import { | ||
| loadOperatingChains, | ||
| init, | ||
| ChainInfo, | ||
| getChainConfig, | ||
| getSigner, | ||
| getContractAddress, | ||
| } from "./env"; | ||
| import { Governance__factory } from "../contract-bindings"; | ||
|
|
||
| const processName = "executeGovernanceVaas"; | ||
|
|
||
| type GovernanceConfig = { | ||
| chainId: ChainId; | ||
| vaa: string; | ||
| }; | ||
|
|
||
| init(); | ||
| const chains = loadOperatingChains(); | ||
| async function run() { | ||
| console.log(`Start ${processName}!`); | ||
|
|
||
| const results = await Promise.all( | ||
| chains.map(async (chain) => { | ||
| let result; | ||
| try { | ||
| result = await executeGovernance(chain); | ||
| } catch (error: unknown) { | ||
| return { chainId: chain.chainId, peerUpdateTxs: [] as string[], error }; | ||
| } | ||
|
|
||
| return result; | ||
| }) | ||
| ); | ||
|
|
||
| for (const result of results) { | ||
| if (!result) { | ||
| continue; | ||
| } | ||
|
|
||
| if ("error" in result) { | ||
| console.error( | ||
| `${processName} failed for chain ${result.chainId}: ${inspect(result.error)}` | ||
| ); | ||
| continue; | ||
| } | ||
|
|
||
| console.log(`${processName} succeeded for chain ${result.chainId}`); | ||
| } | ||
| } | ||
|
|
||
| async function executeGovernance(chain: ChainInfo) { | ||
| const log = (...args: any[]) => console.log(`[${chain.chainId}]`, ...args); | ||
| const { vaa } = await getChainConfig<GovernanceConfig>( | ||
| "governance-vaas", | ||
| chain.chainId | ||
| ); | ||
|
|
||
| const governanceContract = await getGovernanceContract(chain); | ||
|
|
||
| const vaaHex = Buffer.from(vaa, "base64").toString("hex"); | ||
| log("Executing governance with VAA: ", vaaHex); | ||
|
|
||
| const tx = await governanceContract.performGovernance( | ||
| `0x${vaaHex}`, | ||
| {} // overrides | ||
| ); | ||
|
|
||
| log(`Submitted governance transaction: ${tx.hash}`); | ||
|
|
||
| await tx.wait(); | ||
|
|
||
| log("success."); | ||
| } | ||
|
|
||
| run().then(() => console.log("Done!")); | ||
|
|
||
| async function getGovernanceContract(chain: ChainInfo) { | ||
| const signer = await getSigner(chain); | ||
| const governanceAddress = await getContractAddress( | ||
| "GeneralPurposeGovernances", | ||
| chain.chainId | ||
| ); | ||
| return Governance__factory.connect(governanceAddress, signer); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| # Required: "devnet" or "mainnet" | ||
| ENV=devnet | ||
|
|
||
| # Solana RPC URL | ||
| SOLANA_RPC_URL=https://api.devnet.solana.com | ||
|
|
||
| # Commitment level (optional, defaults to "confirmed") | ||
| SOLANA_COMMITMENT=confirmed | ||
|
|
||
| # Ledger derivation path (required for env.ts / Ledger scripts) | ||
| LEDGER_DERIVATION_PATH= | ||
|
|
||
| # Guardian key (required for governance message creation) | ||
| GUARDIAN_KEY= |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| .env | ||
| node_modules | ||
| dist |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| { | ||
| "vaa": "01000000000100403baa70cb8c22af68f1f17ce4790024ef4073a7d380f936e418da36a3ee9e8134515679b2feaff806584edf5a8b1976d11a77b1c1b9323d3596c425652957870100000000a3c9167e00010000000000000000000000000000000000000000000000000000000000000004c815dfbc88fdf79420000000000000000047656e6572616c507572706f7365476f7665726e616e636502000105730eefefc3605ca196155a3ca5594706690d45d01848b8bedfb7d99980f7ec057f97be1c39478e57974f6cc9dbfbeebb0e5ce340c2efd52b8295e889a9ede400050885ee38373250ab21bafa2ea2b6c037b511243608b750fcd6c24bdf31710b1f00018a98013dc14753bff4aa9ba82a9bb6f95835fdc6f97561e4d9eaee76cae462ee00006f776e65720000000000000000000000000000000000000000000000000000000100963bdfa10970171c86e222a176f6f831222031172f2c2de3e511ed95ace40018000102a8f6914e88a1b0e210153ef763ae2b00c2b93d16c124d2c0537a100480000000000008eca6efde0e2d8ffe" | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| [ | ||
| { | ||
| "id": "1/0000000000000000000000000000000000000000000000000000000000000004/17267778771311161006", | ||
| "vaa": "AQAAAAQNAI65VaLhYFpsKDt5UzezT5jFBhRxbGHhBWSuDPIVNyH9HQOlTHSyBDpXhOAR8hy0Wjj1AlPG67AnuGRJNSbIhvYAA03z3U0upcHC0JOnMnmPNSPE+lCKuYF/vHU/FtQ+rg8UIOggAn9/meZLhhlCh7sJN/xIe5Mhnm19psjKz9FfGnIBBUgKKGChLIlRUgb2ZWLGS4OjByi+j6YSExb+5R3UTgJDLdU5K5F54m//HLP4hagpMb314+UWJv7EJfdYeS8Qx94ABvPI4Ae+H3wwH5I/Y65DRYQDtpMGDH6EogkYNN/DyiShB9Pm5gGN0JM7DgNmCHODlV6O7UB3zI2+W9WsnZnCOdkABz4HOBelUsaeBXvLeEgIuDYrE6/4sUWMqlOjB1FY4V9PM+ZG6a5Ts4K9zRcMpT3t1NM4Wjf+CffvBK/C+Wu4MSwACsXQ5JvMpXTe7QAAKaTmtfrH0EYKCwtNkbqJULyLMbC3AQXh677QeaSqZ8/snJqHADClVkfTsuF8u4vm46MEQAcBCxq4Dbif1rNBXVxFeF0JrvFD7dWCPF207pAQzTv43ZRNP+vt+h0tfB1m1xDWMjms4EsEoTB/gGmhZI/pRRW32wUBDHS/dvPBezSpPVaOZx3jhuOBj6XNkAI7aXGyYR5PyrYvNjxOlf3YaMeud9bt4MUjuZY1PdKYqTwNJULRO6g3eBUBDQbuIAf3N/WcmmIIGHXTVMxMPEPI9YHlyxTwIRqzA1NBZrVYnRcc7NSmdLr3bJ4QXwxNi2/fwvdsQSC1YVTmAskBDp3ylnFFrXZGOw+NGUzRl+GbVxMG/PPJe4vCaijmENuwRQkQl6e9/gVboWEk0doI5xV+cNQpzzyMQvtcoMuB6TMBEA49JvrmJ9Tys/OO/l0O8kuPTnBiEQ0MNnOWVJ8JwVTZHNYMxx5xjqEGT+aGZRZ5AtGyaX4/wW1a/A+0UsRGxUQAEf6/+jJtuPGxUHvlJzgUAAMs+/LMsew8VRG/tpU9uH00DZitLKDXogfEyPI0laGix8Pgk81YiF+HkuUZCtKi6WgBEvj+rOA48qWczx1m9wdxFFqtawbG4We+qx0U1ot4ExvhEVJoP8OMPArQnJd78okWn2ekZK+yWuQ8e307OoBl2pkAAAAAAAaA4fgAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAE76N5TcwIbq4gAAAAAAAAAABHZW5lcmFsUHVycG9zZUdvdmVybmFuY2UCAAEFcw7v78NgXKGWFVo8pVlHBmkNRdAYSLi+37fZmYD37AV/l74cOUeOV5dPbMnb++67DlzjQMLv1SuCleiJqe3kAAMIhe44NzJQqyG6+i6itsA3tREkNgi3UPzWwkvfMXELHwAAb3duZXIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABADlm1IlN79/aRsLMXswc43tUQKJlINi1e69RyzvOkpJhAAEAENoIAcyn6QqeAADBb/KGIwA=" | ||
| }, | ||
| { | ||
| "id": "1/0000000000000000000000000000000000000000000000000000000000000004/5665574265595141132", | ||
| "vaa": "AQAAAAQNALq8JasAs0jexvcmn9DUk729hB/Hn+Nb6fiJlgGpkJUvbfCNUF6c/miNTGBdTWiRvZpUugBfOCha1/rZ0qYvCRsBA+ov5I985O9AQgBYllJGuiLjzxQRF7Zu7LL9vbKyuqICMvkHXNpJYe+XWjjrA0/9dzbEeqbCq2aEibFd4UeHIqUBBfeObPyG+EFKwVhjp14aQ3j1RBSBE2LVr2asP3Gh2E2xVgAIqOis+q0pABMF3ES1oC1gVPTDq3hwZYIncP9bQkIBBsvKjIUf6dzQc1nsla2nzO/7hy+3K+zrnI73RVz6ntyCKFxXLJIFqTxbmwSOEn+X+CpR/1/dZ+///MVcHALo8iUAB6mEOYDzEKbOpwqzHBTKetvLtuytUbDjXLfT2JmIGpDPRBnSOjrdWVeV/xEVs/KT6TmSNATemBerVpUgNRWjln0ACFABS7L/OytWxOvEd415GYO1S8Ome7O9OtcdkBqW7qVCf0kKc4y0WCsKKizS/Zl1JB8MtQTRNeQ4M40wg2HCcK4ACTREFSREEj2w1pXIBoj8FStDn75bSWlgVoumYyvmB7JJVuOwB7ao4yreGRFzKrOTyxjkbHMIVKe1MoI/nekjlnkACs1KektGrLvVTZjB/jKT3Ncr0epxAgX7mUoupH8MZZiHHoT/jcUQX5wKajAaAAQIBS0fQDXQc6ndlHw+/1kNpK0ADdiRpv0GABSe4fPPjJUnchYFlXmkh9mR1XBPetoZcvqiCZEdJgtPNqkg9qpK7t9kna247nsdwzTXmvy7ROsSPfsBDmOH9g4AWxmuViggcYPHN9b4KvM+4YOwUU54hWYGOxGMboS+QzVx+l8m975J8V98xsFklAy+ZdPEwg8oR90rorIBEJgM4lkC+jg77/atGLm9KjGZaefyav89MqcBDjBCBVZjQ5Wn6xhljUlemTXwhnsakaZUyjJe2ovCO0dtPylwCxEAEbth2Ejuso+rAhZGkdvTjVVlzC3EqstMCU/X+Re4FQrIGTjNJfpjn1ZeQ/v0Mwuu8TC9gwJJssju0h+A4/t/k/IAEujBi2W5AGxiDXA22u9q3fQdJ3L7W3pXtGRPtuVHh6FUD0pm5n4jaPuWbcnH2F+EwZU/bSacUAZyiJkWxHaGYqwBAAAAAPB6e/4AAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAETqApxu1knAwgAAAAAAAAAABHZW5lcmFsUHVycG9zZUdvdmVybmFuY2UCAAEFcw7v78NgXKGWFVo8pVlHBmkNRdAYSLi+37fZmYD37AV/l74cOUeOV5dPbMnb++67DlzjQMLv1SuCleiJqe3kAAMIhe44NzJQqyG6+i6itsA3tREkNgi3UPzWwkvfMXELHwAAb3duZXIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABADlm1IlN79/aRsLMXswc43tUQKJlINi1e69RyzvOkpJhAAEAENoIAcyn6QqeAEB6EPNaAAA=" | ||
| } | ||
| ] |
Uh oh!
There was an error while loading. Please reload this page.