Skip to content
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

ta/upgradeHelpers #129

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/synthetic-chain/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"execa": "^8.0.1"
},
"devDependencies": {
"@agoric/smart-wallet": "0.5.4-u14.1",
"@types/better-sqlite3": "^7.6.9",
"@types/node": "^18.19.14",
"ava": "^5.3.1",
Expand Down
3 changes: 1 addition & 2 deletions packages/synthetic-chain/src/lib/cliHelper.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,7 @@ export const agoric = {

export const { stdout: agopsLocation } = await $({
shell: true,
cwd: '/usr/src/agoric-sdk',
})`yarn bin agops`;
})`YARN_IGNORE_NODE=1 yarn bin agops`;

export const agops = {
vaults: async (...params) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ import * as path from 'node:path';
import { agd, agoric, agops } from './cliHelper.js';
import { CHAINID, HOME, VALIDATORADDR } from './constants.js';

import type { OfferSpec } from '@agoric/smart-wallet/src/offers.js';
import assert from 'node:assert';

type ERef<T> = T | Promise<T>;

const waitForBootstrap = async () => {
const endpoint = 'localhost';
while (true) {
Expand Down Expand Up @@ -50,7 +55,7 @@ export const waitForBlock = async (times = 1) => {
}
};

export const provisionSmartWallet = async (address, amount) => {
export const provisionSmartWallet = async (address: string, amount: string) => {
console.log(`funding ${address}`);
await agd.tx(
'bank',
Expand Down Expand Up @@ -88,14 +93,14 @@ export const newOfferId = async () => {
return date;
};

export const mkTemp = async template => {
export const mkTemp = async (template: string) => {
const { stdout: data } = await $({
shell: true,
})`mktemp -t ${template}`;
return data;
};

export const calculateWalletState = async addr => {
export const calculateWalletState = async (addr: string) => {
const result = await agoric.follow(
'-lF',
`:published.wallet.${addr}`,
Expand All @@ -117,10 +122,19 @@ export const calculateWalletState = async addr => {
return state;
};

export const executeOffer = async (address, offerPromise) => {
export const executeOffer = async (
address: string,
offerJsonOrStringP: ERef<OfferSpec>,
) => {
const offerPath = await mkTemp('agops.XXX');
const offer = await offerPromise;
await fsp.writeFile(offerPath, offer);
const offerJsonOrString = await offerJsonOrStringP;
const offerJson: OfferSpec =
typeof offerJsonOrString === 'string'
? // this is going to be stringified again but this we we guarantee it's valid JSON
JSON.parse(offerJsonOrString)
: offerJsonOrString;

await fsp.writeFile(offerPath, JSON.stringify(offerJson));

await agops.perf(
'satisfaction',
Expand All @@ -132,25 +146,30 @@ export const executeOffer = async (address, offerPromise) => {
);
};

export const getUser = async user => {
export const getUser = async (user: string) => {
return agd.keys('show', user, '-a', '--keyring-backend=test');
};

export const addUser = async user => {
export const addUser = async (user: string) => {
const userKeyData = await agd.keys('add', user, '--keyring-backend=test');
await fsp.writeFile(`${HOME}/.agoric/${user}.key`, userKeyData.mnemonic);

const userAddress = await getUser(user);
return userAddress;
};

/**
* @params {string} [title]
* @returns {Promise<{ proposal_id: string, voting_end_time: unknown, status: string }>}
*/
export const voteLatestProposalAndWait = async title => {
export const voteLatestProposalAndWait = async (title?: string) => {
await waitForBlock();
let { proposals } = await agd.query('gov', 'proposals');
let { proposals } = (await agd.query('gov', 'proposals')) as {
proposals: {
proposal_id?: string;
id?: string;
voting_end_time: unknown;
status: string;
content: any;
messages: any[];
}[];
};
if (title) {
proposals = proposals.filter(proposal => {
if (proposal.content) {
Expand All @@ -168,7 +187,9 @@ export const voteLatestProposalAndWait = async title => {
}
let lastProposal = proposals.at(-1);

lastProposal || Fail`No proposal found`;
if (!lastProposal) {
throw Fail`No proposal found`;
}

const lastProposalId = lastProposal.proposal_id || lastProposal.id;

Expand All @@ -193,6 +214,8 @@ export const voteLatestProposalAndWait = async title => {
lastProposal = await agd.query('gov', 'proposal', lastProposalId);
}

assert(lastProposal);

lastProposal.status === 'PROPOSAL_STATUS_VOTING_PERIOD' ||
Fail`Latest proposal ${lastProposalId} not in voting period (status=${lastProposal.status})`;

Expand All @@ -217,14 +240,15 @@ export const voteLatestProposalAndWait = async title => {
await waitForBlock()
) {
lastProposal = await agd.query('gov', 'proposal', lastProposalId);
assert(lastProposal);
console.log(
`Waiting for proposal ${lastProposalId} to pass (status=${lastProposal.status})`,
);
}
return { proposal_id: lastProposalId, ...lastProposal };
};

const Fail = (template, ...args) => {
const Fail = (template: any, ...args: any[]) => {
throw Error(String.raw(template, ...args.map(val => String(val))));
};

Expand All @@ -235,7 +259,7 @@ const Fail = (template, ...args) => {
*
* adapted from packages/boot/test/bootstrapTests/supports.js
*/
const parseProposalParts = txt => {
const parseProposalParts = (txt: string) => {
const evals = [
...txt.matchAll(/swingset-core-eval (?<permit>\S+) (?<script>\S+)/g),
].map(m => {
Expand All @@ -253,7 +277,7 @@ const parseProposalParts = txt => {
return { evals, bundles };
};

export const proposalBuilder = async fileName => {
export const proposalBuilder = async (fileName: string) => {
const { stdout: output } = await $({ cwd: '/tmp' })`agoric run ${fileName}`;
const { evals, bundles } = parseProposalParts(output);

Expand All @@ -267,7 +291,7 @@ export const proposalBuilder = async fileName => {
return { evals: evalsWithLocation, bundles };
};

export const installBundle = async (addr, bundlePath) => {
export const installBundle = async (addr: string, bundlePath: string) => {
await agd.tx(
'swingset',
'install-bundle',
Expand All @@ -283,10 +307,10 @@ export const installBundle = async (addr, bundlePath) => {
};

export const submitProposal = async (
scriptPath,
permitPath,
title,
description,
scriptPath: string,
permitPath: string,
title: string,
description: string,
) => {
await agd.tx(
'gov',
Expand Down
2 changes: 1 addition & 1 deletion packages/synthetic-chain/src/lib/core-eval.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {
loadedBundleIds,
readBundles,
txAbbr,
} from './core-eval-support.js';
} from '@agoric/synthetic-chain';
import { step } from './logging.js';

export const staticConfig = {
Expand Down
1 change: 1 addition & 0 deletions packages/synthetic-chain/src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ export * from './econHelpers.js';
export * from './logging.js';
export * from './vat-status.js';
export * from './webAsset.js';
export * from './to-migrate-out/index.js';
140 changes: 140 additions & 0 deletions packages/synthetic-chain/src/lib/to-migrate-out/chain.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
/** @file copied from packages/agoric-cli */
// TODO DRY in https://github.com/Agoric/agoric-sdk/issues/9109
// @ts-check
/* global process */

const agdBinary = 'agd';

/**
* @param {ReadonlyArray<string>} swingsetArgs
* @param {import('./rpc.js').MinimalNetworkConfig & {
* from: string,
* fees?: string,
* dryRun?: boolean,
* verbose?: boolean,
* keyring?: {home?: string, backend: string}
* stdout?: Pick<import('stream').Writable, 'write'>
* execFileSync: typeof import('child_process').execFileSync
* }} opts
*/
export const execSwingsetTransaction = (swingsetArgs, opts) => {
const {
from,
fees,
dryRun = false,
verbose = true,
keyring = undefined,
chainName,
rpcAddrs,
stdout = process.stdout,
execFileSync,
} = opts;
const homeOpt = keyring?.home ? [`--home=${keyring.home}`] : [];
const backendOpt = keyring?.backend
? [`--keyring-backend=${keyring.backend}`]
: [];
const feeOpt = fees ? ['--fees', fees] : [];
const cmd = [`--node=${rpcAddrs[0]}`, `--chain-id=${chainName}`].concat(
homeOpt,
backendOpt,
feeOpt,
[`--from=${from}`, 'tx', 'swingset'],
swingsetArgs,
);

if (dryRun) {
stdout.write(`Run this interactive command in shell:\n\n`);
stdout.write(`${agdBinary} `);
stdout.write(cmd.join(' '));
stdout.write('\n');
} else {
const yesCmd = cmd.concat(['--yes']);
if (verbose) console.log('Executing ', yesCmd);
const out = execFileSync(agdBinary, yesCmd, { encoding: 'utf-8' });

// agd puts this diagnostic on stdout rather than stderr :-/
// "Default sign-mode 'direct' not supported by Ledger, using sign-mode 'amino-json'.
if (out.startsWith('Default sign-mode')) {
const stripDiagnostic = out.replace(/^Default[^\n]+\n/, '');
return stripDiagnostic;
}
return out;
}
};
harden(execSwingsetTransaction);

/**
* @param {import('./rpc.js').MinimalNetworkConfig & {
* execFileSync: typeof import('child_process').execFileSync,
* delay: (ms: number) => Promise<void>,
* period?: number,
* retryMessage?: string,
* }} opts
* @returns {<T>(l: (b: { time: string, height: string }) => Promise<T>) => Promise<T>}
*/
export const pollBlocks = opts => async lookup => {
const { execFileSync, delay, rpcAddrs, period = 3 * 1000 } = opts;
assert(execFileSync, 'missing execFileSync');
const { retryMessage } = opts;

const nodeArgs = [`--node=${rpcAddrs[0]}`];

await null; // separate sync prologue

for (;;) {
const sTxt = execFileSync(agdBinary, ['status', ...nodeArgs]);
const status = JSON.parse(sTxt.toString());
const {
SyncInfo: { latest_block_time: time, latest_block_height: height },
} = status;
try {
// see await null above
const result = await lookup({ time, height });
return result;
} catch (_err) {
console.error(
time,
retryMessage || 'not in block',
height,
'retrying...',
);
await delay(period);
}
}
};

/**
* @param {string} txhash
* @param {import('./rpc.js').MinimalNetworkConfig & {
* execFileSync: typeof import('child_process').execFileSync,
* delay: (ms: number) => Promise<void>,
* period?: number,
* }} opts
*/
export const pollTx = async (txhash, opts) => {
const { execFileSync, rpcAddrs, chainName } = opts;
assert(execFileSync, 'missing execFileSync in pollTx');

const nodeArgs = [`--node=${rpcAddrs[0]}`];
const outJson = ['--output', 'json'];

const lookup = async () => {
const out = execFileSync(
agdBinary,
[
'query',
'tx',
txhash,
`--chain-id=${chainName}`,
...nodeArgs,
...outJson,
],
{ stdio: ['ignore', 'pipe', 'ignore'] },
);
// XXX this type is defined in a .proto file somewhere
/** @type {{ height: string, txhash: string, code: number, timestamp: string }} */
const info = JSON.parse(out.toString());
return info;
};
return pollBlocks({ ...opts, retryMessage: 'tx not in block' })(lookup);
};
23 changes: 23 additions & 0 deletions packages/synthetic-chain/src/lib/to-migrate-out/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/* global setTimeout */
import fetch from 'node-fetch';
import { execFileSync } from 'child_process';
import { makeWalletUtils } from './wallet.js';

export const localNetworkConfig = {
rpcAddrs: ['http://0.0.0.0:26657'],
chainName: 'agoriclocal',
};

/**
* Resolve after a delay in milliseconds.
*
* @param {number} ms
* @returns {Promise<void>}
*/
const delay = ms => new Promise(resolve => setTimeout(() => resolve(), ms));

// eslint-disable-next-line @jessie.js/safe-await-separator -- buggy version
export const walletUtils = await makeWalletUtils(
{ delay, execFileSync, fetch },
localNetworkConfig,
);