Skip to content

Commit

Permalink
feat: add upgrade-15 (proposal 74)
Browse files Browse the repository at this point in the history
  • Loading branch information
0xpatrickdev committed May 15, 2024
1 parent 7646a5c commit 81c0271
Show file tree
Hide file tree
Showing 11 changed files with 2,684 additions and 0 deletions.
5 changes: 5 additions & 0 deletions proposals/74:upgrade-15/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Proposal to upgrade the chain software to upgrade-14

This software upgrade executes core proposals during the upgrade block, as
defined by the `agoric-upgrade-14` upgrade handler. See `upgrade14Handler` in
`agoric-sdk/golang/cosmos/app/app.go`
36 changes: 36 additions & 0 deletions proposals/74:upgrade-15/exit-reclaim.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import test from 'ava';
import { $ } from 'execa';
import { execFileSync } from 'node:child_process';
import { makeAgd, waitForBlock } from './synthetic-chain-excerpt.js';

const offerId = 'bad-invitation-15'; // cf. prepare.sh
const from = 'gov1';

test('exitOffer tool reclaims stuck payment', async t => {
const showAndExec = (file, args, opts) => {
console.log('$', file, ...args);
return execFileSync(file, args, opts);
};
const agd = makeAgd({ execFileSync: showAndExec }).withOpts({
keyringBackend: 'test',
});

const addr = await agd.lookup(from);
t.log(from, 'addr', addr);

const getBalance = async target => {
const { balances } = await agd.query(['bank', 'balances', addr]);
const { amount } = balances.find(({ denom }) => denom === target);
return Number(amount);
};

const before = await getBalance('uist');
t.log('uist balance before:', before);

await $`node ./exitOffer.js --id ${offerId} --from ${from}`;

await waitForBlock(2);
const after = await getBalance('uist');
t.log('uist balance after:', after);
t.true(after > before);
});
97 changes: 97 additions & 0 deletions proposals/74:upgrade-15/exitOffer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// Note: limit imports to node modules for portability
import { parseArgs, promisify } from 'node:util';
import { execFile } from 'node:child_process';
import { writeFile, mkdtemp, rm } from 'node:fs/promises';
import { join } from 'node:path';

const options = /** @type {const} */ ({
id: { type: 'string' },
from: { type: 'string' },
bin: { type: 'string', default: '/usr/src/agoric-sdk/node_modules/.bin' },
});

const Usage = `
Try to exit an offer, reclaiming any associated payments.
node exitOffer.js --id ID --from FROM [--bin PATH]
Options:
--id <offer id>
--from <address or key name>
--bin <path to agoric and agd> default: ${options.bin.default}
`;

const badUsage = () => {
const reason = new Error(Usage);
reason.name = 'USAGE';
throw reason;
};

const { stringify: q } = JSON;
// limited to JSON data: no remotables/promises; no undefined.
const toCapData = data => ({ body: `#${q(data)}`, slots: [] });

const { entries } = Object;
/**
* @param {Record<string, string>} obj - e.g. { color: 'blue' }
* @returns {string[]} - e.g. ['--color', 'blue']
*/
const flags = obj =>
entries(obj)
.map(([k, v]) => [`--${k}`, v])
.flat();

const execP = promisify(execFile);

const showAndRun = (file, args) => {
console.log('$', file, ...args);
return execP(file, args);
};

const withTempFile = async (tail, fn) => {
const tmpDir = await mkdtemp('offers-');
const tmpFile = join(tmpDir, tail);
try {
const result = await fn(tmpFile);
return result;
} finally {
await rm(tmpDir, { recursive: true, force: true }).catch(err =>
console.error(err),
);
}
};

const doAction = async (action, from) => {
await withTempFile('offer.json', async tmpOffer => {
await writeFile(tmpOffer, q(toCapData(action)));

const out = await showAndRun('agoric', [
'wallet',
...flags({ 'keyring-backend': 'test' }),
'send',
...flags({ offer: tmpOffer, from }),
]);
return out.stdout;
});
};

const main = async (argv, env) => {
const { values } = parseArgs({ args: argv.slice(2), options });
const { id: offerId, from, bin } = values;
(offerId && from) || badUsage();

env.PATH = `${bin}:${env.PATH}`;
const action = { method: 'tryExitOffer', offerId };
const out = await doAction(action, from);
console.log(out);
};

main(process.argv, process.env).catch(e => {
if (e.name === 'USAGE' || e.code === 'ERR_PARSE_ARGS_UNKNOWN_OPTION') {
console.error(e.message);
} else {
console.error(e);
}
process.exit(1);
});
16 changes: 16 additions & 0 deletions proposals/74:upgrade-15/initial.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import test from 'ava';

import { getVatDetails } from '@agoric/synthetic-chain';

const vats = {
walletFactory: { incarnation: 3 },
zoe: { incarnation: 1 },
};

test(`vat details`, async t => {
await null;
for (const [vatName, expected] of Object.entries(vats)) {
const actual = await getVatDetails(vatName);
t.like(actual, expected, `${vatName} details mismatch`);
}
});
23 changes: 23 additions & 0 deletions proposals/74:upgrade-15/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"agoricProposal": {
"releaseNotes": false,
"sdkImageTag": "unreleased",
"planName": "agoric-upgrade-15",
"upgradeInfo": {},
"type": "Software Upgrade Proposal"
},
"type": "module",
"license": "Apache-2.0",
"dependencies": {
"@agoric/synthetic-chain": "^0.0.10",
"ava": "^5.3.1"
},
"ava": {
"concurrency": 1,
"serial": true
},
"scripts": {
"agops": "yarn --cwd /usr/src/agoric-sdk/ --silent agops"
},
"packageManager": "yarn@4.1.0"
}
29 changes: 29 additions & 0 deletions proposals/74:upgrade-15/prepare.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#!/bin/bash

# Exit when any command fails
set -uxeo pipefail

# Place here any actions that should happen before the upgrade is proposed. The
# actions are executed in the previous chain software, and the effects are
# persisted so they can be used in the steps after the upgrade is complete,
# such as in the "use" or "test" steps, or further proposal layers.

printISTBalance() {
addr=$(agd keys show -a "$1" --keyring-backend=test)
agd query bank balances "$addr" -o json \
| jq -c '.balances[] | select(.denom=="uist")'

}

echo TEST: Offer with bad invitation
printISTBalance gov1

badInvitationOffer=$(mktemp)
cat > "$badInvitationOffer" << 'EOF'
{"body":"#{\"method\":\"executeOffer\",\"offer\":{\"id\":\"bad-invitation-15\",\"invitationSpec\":{\"callPipe\":[[\"badMethodName\"]],\"instancePath\":[\"reserve\"],\"source\":\"agoricContract\"},\"proposal\":{\"give\":{\"Collateral\":{\"brand\":\"$0.Alleged: IST brand\",\"value\":\"+15000\"}}}}}","slots":["board0257"]}
EOF

PATH=/usr/src/agoric-sdk/node_modules/.bin:$PATH
agops perf satisfaction --keyring-backend=test send --executeOffer "$badInvitationOffer" --from gov1 || true

printISTBalance gov1
166 changes: 166 additions & 0 deletions proposals/74:upgrade-15/synthetic-chain-excerpt.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
/**
* @file work-around: importing @agoric/synthetic-chain hangs XXX
*/
// @ts-check
import { $ } from 'execa';

const waitForBootstrap = async () => {
const endpoint = 'localhost';
while (true) {
const { stdout: json } = await $({
reject: false,
})`curl -s --fail -m 15 ${`${endpoint}:26657/status`}`;

if (json.length === 0) {
continue;
}

const data = JSON.parse(json);

if (data.jsonrpc !== '2.0') {
continue;
}

const lastHeight = data.result.sync_info.latest_block_height;

if (lastHeight !== '1') {
return lastHeight;
}

await new Promise(r => setTimeout(r, 2000));
}
};

export const waitForBlock = async (times = 1) => {
console.log(times);
let time = 0;
while (time < times) {
const block1 = await waitForBootstrap();
while (true) {
const block2 = await waitForBootstrap();

if (block1 !== block2) {
console.log('block produced');
break;
}

await new Promise(r => setTimeout(r, 1000));
}
time += 1;
}
};

const { freeze } = Object;

const agdBinary = 'agd';

/**
* @param {{execFileSync: typeof import('child_process').execFileSync }} io
* @returns
*/
export const makeAgd = ({ execFileSync }) => {
/**
* @param {{
* home?: string;
* keyringBackend?: string;
* rpcAddrs?: string[];
* }} opts
*/
const make = ({ home, keyringBackend, rpcAddrs } = {}) => {
const keyringArgs = [
...(home ? ['--home', home] : []),
...(keyringBackend ? [`--keyring-backend`, keyringBackend] : []),
];
if (rpcAddrs) {
assert.equal(
rpcAddrs.length,
1,
'XXX rpcAddrs must contain only one entry',
);
}
const nodeArgs = [...(rpcAddrs ? [`--node`, rpcAddrs[0]] : [])];

const exec = (args, opts) => execFileSync(agdBinary, args, opts).toString();

const outJson = ['--output', 'json'];

const ro = freeze({
status: async () => JSON.parse(exec([...nodeArgs, 'status'])),
query: async qArgs => {
const out = exec(['query', ...qArgs, ...nodeArgs, ...outJson], {
encoding: 'utf-8',
stdio: ['ignore', 'pipe', 'ignore'],
});

try {
return JSON.parse(out);
} catch (e) {
console.error(e);
console.info('output:', out);
}
},
});
const nameHub = freeze({
/**
* NOTE: synchronous I/O
*/
lookup: (...path) => {
if (!Array.isArray(path)) {
// TODO: use COND || Fail``
throw TypeError();
}
if (path.length !== 1) {
throw Error(`path length limited to 1: ${path.length}`);
}
const [name] = path;
const txt = exec(['keys', 'show', `--address`, name, ...keyringArgs]);
return txt.trim();
},
});
const rw = freeze({
/**
* @param {string[]} txArgs
* @param {{ chainId: string, from: string, yes?: boolean }} opts
*/
tx: async (txArgs, { chainId, from, yes }) => {
const yesArg = yes ? ['--yes'] : [];
const args = [
...nodeArgs,
...[`--chain-id`, chainId],
...keyringArgs,
...[`--from`, from],
'tx',
...['--broadcast-mode', 'block'],
...['--gas', 'auto'],
...['--gas-adjustment', '1.3'],
...txArgs,
...yesArg,
...outJson,
];
const out = exec(args);
try {
return JSON.parse(out);
} catch (e) {
console.error(e);
console.info('output:', out);
}
},
...ro,
...nameHub,
readOnly: () => ro,
nameHub: () => nameHub,
keys: {
add: (name, mnemonic) => {
return execFileSync(
agdBinary,
[...keyringArgs, 'keys', 'add', name, '--recover'],
{ input: mnemonic },
).toString();
},
},
withOpts: opts => make({ home, keyringBackend, rpcAddrs, ...opts }),
});
return rw;
};
return make();
};
6 changes: 6 additions & 0 deletions proposals/74:upgrade-15/test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/bin/bash

# Place here any test that should be executed using the executed proposal.
# The effects of this step are not persisted in further proposal layers.

yarn ava
12 changes: 12 additions & 0 deletions proposals/74:upgrade-15/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"moduleResolution": "node",
"allowJs": true,
"checkJs": true,
"strict": false,
"strictNullChecks": true,
"noImplicitThis": true
}
}
Loading

0 comments on commit 81c0271

Please sign in to comment.