-
Notifications
You must be signed in to change notification settings - Fork 230
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #6187 from Agoric/dc-test-starter-ist
test(cosmic-swingset): pay 10 BLD for account with 0.25 IST to start
- Loading branch information
Showing
4 changed files
with
304 additions
and
40 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,153 @@ | ||
/* eslint-disable no-await-in-loop */ | ||
const { freeze, entries } = Object; | ||
|
||
const onlyStderr = ['ignore', 'ignore', 'inherit']; | ||
const noOutput = ['ignore', 'ignore', 'ignore']; | ||
// const noisyDebug = ['ignore', 'inherit', 'inherit']; | ||
|
||
export const pspawn = | ||
(bin, { spawn, cwd }) => | ||
(args = [], opts = {}) => { | ||
let child; | ||
const exit = new Promise((resolve, reject) => { | ||
// console.debug('spawn', bin, args, { cwd: makefileDir, ...opts }); | ||
child = spawn(bin, args, { cwd, ...opts }); | ||
child.addListener('exit', code => { | ||
if (code !== 0) { | ||
reject(Error(`exit ${code} from: ${bin} ${args}`)); | ||
return; | ||
} | ||
resolve(0); | ||
}); | ||
}); | ||
return { child, exit }; | ||
}; | ||
|
||
/** | ||
* Shared state for tests using scenario2 chain in ../ | ||
* | ||
* @param {object} io | ||
* @param {*} io.pspawnMake promise-style spawn of 'make' with cwd set | ||
* @param {*} io.pspawnAgd promise-style spawn of 'ag-chain-cosmos' with cwd set | ||
* @param {typeof console.log} io.log | ||
*/ | ||
export const makeScenario2 = ({ pspawnMake, pspawnAgd, log }) => { | ||
const runMake = (args, opts = { stdio: onlyStderr }) => { | ||
// console.debug('make', ...args); | ||
log('make', ...args); | ||
|
||
return pspawnMake(args, opts).exit; | ||
}; | ||
|
||
// {X: 1} => ['X=1'], using JSON.stringify to mitigate injection risks. | ||
const bind = obj => | ||
entries(obj) | ||
.filter(([_n, v]) => typeof v !== 'undefined') | ||
.map(([n, v]) => [`${n}=${JSON.stringify(v)}`]) | ||
.flat(); | ||
|
||
return freeze({ | ||
runMake, | ||
setup: () => runMake(['scenario2-setup'], { stdio: noOutput }), | ||
runToHalt: ({ | ||
BLOCKS_TO_RUN = undefined, | ||
INITIAL_HEIGHT = undefined, | ||
} = {}) => | ||
runMake([ | ||
'scenario2-run-chain-to-halt', | ||
...bind({ BLOCKS_TO_RUN, INITIAL_HEIGHT }), | ||
]), | ||
export: () => | ||
pspawnAgd(['export', '--home=t1/n0'], { stdio: onlyStderr }).exit, | ||
}); | ||
}; | ||
|
||
/** | ||
* Wallet utilities for scenario2. | ||
* | ||
* @param {object} io | ||
* @param {*} io.runMake from makeScenario2 above | ||
* @param {*} io.pspawnAgd as to makeScenario2 above | ||
* @param {(ms: number) => Promise<void>} io.delay | ||
* @param {typeof console.log} io.log | ||
*/ | ||
export const makeWalletTool = ({ runMake, pspawnAgd, delay, log }) => { | ||
/** | ||
* @param {string[]} args | ||
* @returns {Promise<any>} JSON.parse of stdout of `ag-chain-cosmos query <...args>` | ||
* @throws if agd exits non-0 or gives empty output | ||
*/ | ||
const query = async args => { | ||
const parts = []; | ||
const cmd = pspawnAgd(['query', ...args]); | ||
cmd.child.stdout.on('data', chunk => parts.push(chunk)); | ||
await cmd.exit; | ||
const txt = parts.join('').trim(); | ||
if (txt === '') { | ||
throw Error(`empty output from: query ${args}`); | ||
} | ||
return JSON.parse(txt); | ||
}; | ||
|
||
const queryBalance = addr => | ||
query(['bank', 'balances', addr, '--output', 'json']).then(b => { | ||
console.log(addr, b); | ||
return b; | ||
}); | ||
|
||
let currentHeight; | ||
const waitForBlock = async (why, targetHeight, relative = false) => { | ||
if (relative) { | ||
if (typeof currentHeight === 'undefined') { | ||
throw Error('cannot use relative before starting'); | ||
} | ||
targetHeight += currentHeight; | ||
} | ||
for (;;) { | ||
try { | ||
const info = await query(['block']); | ||
currentHeight = Number(info?.block?.header?.height); | ||
if (currentHeight >= targetHeight) { | ||
log(info?.block?.header?.time, ' block ', currentHeight); | ||
return currentHeight; | ||
} | ||
console.log(why, ':', currentHeight, '<', targetHeight, '5 sec...'); | ||
} catch (reason) { | ||
console.warn(why, '2:', reason?.message, '5sec...'); | ||
} | ||
await delay(5000); | ||
} | ||
}; | ||
|
||
// one tx per block per address | ||
const myTurn = new Map(); // addr -> blockHeight | ||
const waitMyTurn = async (why, addr) => { | ||
const lastTurn = myTurn.get(addr) || 0; | ||
const nextHeight = await waitForBlock(why, lastTurn + 1); | ||
myTurn.set(addr, nextHeight); | ||
}; | ||
|
||
// {X: 1} => ['X=1'], using JSON.stringify to mitigate injection risks. | ||
const bind = obj => | ||
entries(obj) | ||
.filter(([_n, v]) => typeof v !== 'undefined') | ||
.map(([n, v]) => [`${n}=${JSON.stringify(v)}`]) | ||
.flat(); | ||
|
||
return freeze({ | ||
query, | ||
queryBalance, | ||
waitForBlock, | ||
fundAccount: async (ACCT_ADDR, FUNDS) => { | ||
const a4 = ACCT_ADDR.slice(-4); | ||
await waitMyTurn(`fund ${a4}`, 'bootstrap'); | ||
await runMake([...bind({ ACCT_ADDR, FUNDS }), 'fund-acct']); | ||
await waitForBlock(`${a4}'s funds to appear`, 2, true); | ||
return queryBalance(ACCT_ADDR); | ||
}, | ||
provisionMine: ACCT_ADDR => | ||
waitMyTurn('provision', ACCT_ADDR).then(() => | ||
runMake([...bind({ ACCT_ADDR }), 'provision-my-acct']), | ||
), | ||
}); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,45 +1,41 @@ | ||
import test from 'ava'; | ||
import { spawn } from 'child_process'; | ||
import path from 'path'; | ||
|
||
const filename = new URL(import.meta.url).pathname; | ||
const dirname = path.dirname(filename); | ||
// Use ambient authority only in test.before() | ||
import { spawn as ambientSpawn } from 'child_process'; | ||
import * as ambientPath from 'path'; | ||
|
||
test('make and exec', async t => { | ||
await new Promise(resolve => | ||
spawn('make', ['scenario2-setup'], { | ||
cwd: `${dirname}/..`, | ||
stdio: ['ignore', 'ignore', 'inherit'], | ||
}).addListener('exit', code => { | ||
t.is(code, 0, 'make scenario2-setup exits successfully'); | ||
resolve(); | ||
}), | ||
); | ||
await new Promise(resolve => | ||
spawn('bin/ag-chain-cosmos', { | ||
cwd: `${dirname}/..`, | ||
stdio: ['ignore', 'ignore', 'inherit'], | ||
}).addListener('exit', code => { | ||
t.is(code, 0, 'exec exits successfully'); | ||
resolve(); | ||
}), | ||
); | ||
await new Promise(resolve => | ||
spawn('make', ['scenario2-run-chain-to-halt'], { | ||
cwd: `${dirname}/..`, | ||
stdio: ['ignore', 'ignore', 'inherit'], | ||
}).addListener('exit', code => { | ||
t.is(code, 0, 'make scenario2-run-chain-to-halt is successful'); | ||
resolve(); | ||
}), | ||
import { makeScenario2, pspawn } from './scenario2.js'; | ||
|
||
test.before(async t => { | ||
const filename = new URL(import.meta.url).pathname; | ||
const dirname = ambientPath.dirname(filename); | ||
const makefileDir = ambientPath.join(dirname, '..'); | ||
|
||
const io = { spawn: ambientSpawn, cwd: makefileDir }; | ||
const pspawnMake = pspawn('make', io); | ||
const pspawnAgd = pspawn('bin/ag-chain-cosmos', io); | ||
const scenario2 = makeScenario2({ pspawnMake, pspawnAgd, log: t.log }); | ||
await scenario2.setup(); | ||
|
||
t.context = { scenario2, pspawnAgd }; | ||
}); | ||
|
||
test.serial('make and exec', async t => { | ||
const { pspawnAgd, scenario2 } = t.context; | ||
t.log('exec agd'); | ||
t.is(await pspawnAgd([]).exit, 0, 'exec agd exits successfully'); | ||
t.log('run chain to halt'); | ||
t.is( | ||
await scenario2.runToHalt(), | ||
0, | ||
'make scenario2-run-chain-to-halt is successful', | ||
); | ||
await new Promise(resolve => | ||
spawn('bin/ag-chain-cosmos', ['export', '--home=t1/n0'], { | ||
cwd: `${dirname}/..`, | ||
stdio: ['ignore', 'ignore', 'inherit'], | ||
}).addListener('exit', code => { | ||
t.is(code, 0, 'export exits successfully'); | ||
resolve(); | ||
}), | ||
t.log('resume chain and halt'); | ||
t.is( | ||
await scenario2.runToHalt(), | ||
0, | ||
'make scenario2-run-chain-to-halt succeeds again', | ||
); | ||
t.log('export'); | ||
t.is(await scenario2.export(), 0, 'export exits successfully'); | ||
}); |
105 changes: 105 additions & 0 deletions
105
packages/cosmic-swingset/test/test-provision-smartwallet.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
/* global setTimeout */ | ||
import test from 'ava'; | ||
|
||
// Use ambient authority only in test.before() | ||
import { spawn as ambientSpawn } from 'child_process'; | ||
import * as ambientPath from 'path'; | ||
import * as ambientFs from 'fs'; | ||
|
||
import { makeScenario2, makeWalletTool, pspawn } from './scenario2.js'; | ||
|
||
// module account address for 'vbank/provision'; aka "megz" | ||
// | ||
// It seems to be some sort of hash of the name, 'vbank/provision'. | ||
// Lack of documentation is a known issue: | ||
// https://github.com/cosmos/cosmos-sdk/issues/8411 | ||
// | ||
// In `startWalletFactory.js` we have: | ||
// `E(bankManager).getModuleAccountAddress('vbank/provision')` | ||
// Then in `vat-bank.js` we have a `VBANK_GET_MODULE_ACCOUNT_ADDRESS` | ||
// call across the bridge to golang; `vbank.go` handles it | ||
// by way of `GetModuleAccountAddress` which calls into the cosmos-sdk | ||
// `x/auth` module... over hill and dale, we seem to end up | ||
// with `crypto.AddressHash([]byte(name))` at | ||
// https://github.com/cosmos/cosmos-sdk/blob/512953cd689fd96ef454e424c81c1a0da5782074/x/auth/types/account.go#L158 | ||
// | ||
// Whether this implementation is even correct seems to be | ||
// at issue: | ||
// ModuleAccount addresses don't follow ADR-028 | ||
// https://github.com/cosmos/cosmos-sdk/issues/13782 Nov 2022 | ||
const provisionPoolModuleAccount = | ||
'agoric1megzytg65cyrgzs6fvzxgrcqvwwl7ugpt62346'; | ||
|
||
test.before(async t => { | ||
const filename = new URL(import.meta.url).pathname; | ||
const dirname = ambientPath.dirname(filename); | ||
const makefileDir = ambientPath.join(dirname, '..'); | ||
|
||
const delay = ms => new Promise(resolve => setTimeout(resolve, ms)); | ||
|
||
const io = { spawn: ambientSpawn, cwd: makefileDir }; | ||
const pspawnMake = pspawn('make', io); | ||
const pspawnAgd = pspawn('bin/ag-chain-cosmos', io); | ||
const scenario2 = makeScenario2({ pspawnMake, pspawnAgd, delay, log: t.log }); | ||
const walletTool = makeWalletTool({ | ||
runMake: scenario2.runMake, | ||
pspawnAgd, | ||
delay, | ||
log: t.log, | ||
}); | ||
await scenario2.setup(); | ||
|
||
const { readFile } = ambientFs.promises; | ||
const readItem = f => readFile(f, 'utf-8').then(line => line.trim()); | ||
const soloAddr = await readItem('./t1/8000/ag-cosmos-helper-address'); | ||
const bootstrapAddr = await readItem('./t1/bootstrap-address'); | ||
// console.debug('scenario2 addresses', { soloAddr, bootstrapAddr }); | ||
|
||
t.context = { scenario2, walletTool, pspawnAgd, bootstrapAddr, soloAddr }; | ||
}); | ||
|
||
// SKIP: struggling with timing issues resulting in one of... | ||
// Error: cannot grab 250000uist coins: 0uist is smaller than 250000uist: insufficient funds | ||
// error: code = NotFound desc = account agoric1mhu... not found | ||
// Sometimes I can get this test to work alone, but not | ||
// if run with the test above. | ||
// TODO: https://github.com/Agoric/agoric-sdk/issues/6766 | ||
test.skip('integration test: smart wallet provision', async t => { | ||
const { scenario2, walletTool, soloAddr } = t.context; | ||
|
||
const enoughBlocksToProvision = 7; | ||
const provision = async () => { | ||
// idea: scenario2.waitForBlock(2, true); // let bootstrap account settle down. | ||
// idea: console.log('bootstrap ready', scenario2.queryBalance(bootstrapAddr)); | ||
t.log('Fund pool with USDC'); | ||
await walletTool.fundAccount( | ||
provisionPoolModuleAccount, | ||
`${234e6}ibc/usdc1234`, | ||
); | ||
t.log('Fund user account with some BLD'); | ||
await walletTool.fundAccount(soloAddr, `${123e6}ubld`); | ||
t.log('Provision smart wallet'); | ||
await walletTool.provisionMine(soloAddr, soloAddr); | ||
|
||
await walletTool.waitForBlock( | ||
'provision to finish', | ||
enoughBlocksToProvision, | ||
true, | ||
); | ||
return walletTool.queryBalance(soloAddr); | ||
}; | ||
|
||
const queryGrace = 6; // time to query state before shutting down | ||
const [_run, addrQ] = await Promise.all([ | ||
scenario2.runToHalt({ | ||
BLOCKS_TO_RUN: enoughBlocksToProvision + queryGrace, | ||
}), | ||
provision(), | ||
]); | ||
|
||
t.log('verify 10BLD spent, 0.25 IST received'); | ||
t.deepEqual(addrQ.balances, [ | ||
{ amount: `${(123 - 10) * 1e6}`, denom: 'ubld' }, | ||
{ amount: `${0.25 * 1e6}`, denom: 'uist' }, | ||
]); | ||
}); |