diff --git a/packages/cosmic-swingset/Makefile b/packages/cosmic-swingset/Makefile index e831562b1ca..bba4c7102af 100644 --- a/packages/cosmic-swingset/Makefile +++ b/packages/cosmic-swingset/Makefile @@ -83,6 +83,21 @@ scenario2-reset-client: $(AG_SOLO) init t1/$(BASE_PORT) --webport=$(BASE_PORT) $(MAKE) set-local-gci-ingress +scenario2-client-owner: t1/$(BASE_PORT)/owner + $(AGCH) --home=t1/$(BASE_PORT)/owner tx authz grant $$(cat t1/$(BASE_PORT)/ag-cosmos-helper-address) \ + generic --msg-type=/agoric.swingset.MsgDeliverInbound --from=solo-owner--keyring-backend=test \ + --gas=auto --gas-adjustment=1.2 --broadcast-mode=block --yes --chain-id=$(CHAIN_ID) + + +scenario2-fee-account: t1/$(BASE_PORT)/owner + $(AGCH) --home=t1/$(BASE_PORT)/owner keys --keyring-backend=test show -a owner > t1/$(BASE_PORT)/cosmos-fee-account + +scenario2-client-account: t1/$(BASE_PORT)/owner + $(AGCH) --home=t1/$(BASE_PORT)/owner keys --keyring-backend=test show -a owner > t1/$(BASE_PORT)/cosmos-client-account + +t1/$(BASE_PORT)/owner: + $(AGCH) --home=t1/$(BASE_PORT)/owner keys --keyring-backend=test add owner + # Provision and start a client. scenario2-run-client: t1-provision-one-with-powers t1-start-ag-solo @@ -106,7 +121,9 @@ wait-for-cosmos: @echo ' done!' t1-provision-one-with-powers: wait-for-cosmos - addr=$$(cat t1/$(BASE_PORT)/ag-cosmos-helper-address); \ + @addrfile=t1/$(BASE_PORT)/cosmos-client-account; \ + test -f $$addrfile || addrfile=t1/$(BASE_PORT)/ag-cosmos-helper-address; \ + addr=$$(cat $$addrfile); \ $(AGCH) --home=t1/bootstrap query swingset egress $$addr --chain-id=$(CHAIN_ID) || \ { $(AGCH) --home=t1/bootstrap tx bank send --keyring-backend=test --from=bootstrap \ --gas=auto --gas-adjustment=1.2 --broadcast-mode=block --yes --chain-id=$(CHAIN_ID) \ @@ -114,9 +131,12 @@ t1-provision-one-with-powers: wait-for-cosmos $(AGCH) --home=t1/bootstrap tx swingset provision-one --keyring-backend=test --from=bootstrap \ --gas=auto --gas-adjustment=1.2 --broadcast-mode=block --yes --chain-id=$(CHAIN_ID) \ t1/$(BASE_PORT) $$addr $(AGORIC_POWERS) | tee /dev/stderr | grep -q '"code":0'; } + t1-provision-one: wait-for-cosmos - addr=$$(cat t1/$(BASE_PORT)/ag-cosmos-helper-address); \ + @addrfile=t1/$(BASE_PORT)/cosmos-client-account; \ + test -f $$addrfile || addrfile=t1/$(BASE_PORT)/ag-cosmos-helper-address; \ + addr=$$(cat $$addrfile); \ $(AGCH) --home=t1/bootstrap query swingset egress $$addr --chain-id=$(CHAIN_ID) || \ { $(AGCH) --home=t1/bootstrap tx bank send --keyring-backend=test --from=bootstrap \ --gas=auto --gas-adjustment=1.2 --broadcast-mode=block --yes --chain-id=$(CHAIN_ID) \ @@ -125,8 +145,27 @@ t1-provision-one: wait-for-cosmos --gas=auto --gas-adjustment=1.2 --broadcast-mode=block --yes --chain-id=$(CHAIN_ID) \ t1/$(BASE_PORT) $$addr | tee /dev/stderr | grep -q '"code":0'; } +t1/$(BASE_PORT)/cosmos-client-account.setup: + test ! -f t1/$(BASE_PORT)/cosmos-client-account || \ + $(AGCH) --home=t1/$(BASE_PORT)/owner --keyring-backend=test tx authz grant \ + --gas=auto --gas-adjustment=1.2 --broadcast-mode=block --yes --chain-id=$(CHAIN_ID) --from=owner \ + $$(cat t1/$(BASE_PORT)/ag-cosmos-helper-address) generic --msg-type=/agoric.swingset.MsgDeliverInbound + date > $@ + +t1/$(BASE_PORT)/cosmos-fee-account.setup: + test ! -f t1/$(BASE_PORT)/cosmos-fee-account || \ + $(AGCH) --home=t1/$(BASE_PORT)/owner --keyring-backend=test tx feegrant grant \ + --gas=auto --gas-adjustment=1.2 --broadcast-mode=block --yes --chain-id=$(CHAIN_ID) --from=owner \ + --period=5 --period-limit=200urun $$(cat t1/$(BASE_PORT)/cosmos-fee-account) $$(cat t1/$(BASE_PORT)/ag-cosmos-helper-address) + date > $@ + # Actually start the ag-solo. -t1-start-ag-solo: +t1-start-ag-solo: t1/$(BASE_PORT)/cosmos-client-account.setup t1/$(BASE_PORT)/cosmos-fee-account.setup + addr=$$(cat t1/$(BASE_PORT)/ag-cosmos-helper-address); \ + $(AGCH) query auth account $$addr >/dev/null 2>&1 || \ + $(AGCH) --home=t1/bootstrap tx bank send --keyring-backend=test --from=bootstrap \ + --gas=auto --gas-adjustment=1.2 --broadcast-mode=block --yes --chain-id=$(CHAIN_ID) \ + bootstrap $$addr 1urun cd t1/$(BASE_PORT) && $(AG_SOLO) start # scenario3 is a single JS process without any Golang. However, diff --git a/packages/solo/src/chain-cosmos-sdk.js b/packages/solo/src/chain-cosmos-sdk.js index 70bc57ed4d8..ce589366a8a 100644 --- a/packages/solo/src/chain-cosmos-sdk.js +++ b/packages/solo/src/chain-cosmos-sdk.js @@ -16,15 +16,15 @@ const log = anylogger('chain-cosmos-sdk'); const HELPER = 'ag-cosmos-helper'; const FAUCET_ADDRESS = - '#faucet channel on Discord (https://agoric.com/discord)'; + 'the appropriate faucet channel on Discord (https://agoric.com/discord)'; -const adviseEgress = myAddr => +const adviseEgress = egressAddr => `\ Send: - !faucet client ${myAddr} + !faucet client ${egressAddr} to ${FAUCET_ADDRESS}`; @@ -43,11 +43,43 @@ const CANCEL_USE_DEFAULT = { }, }; +const makeTempFile = async (prefix, contents) => { + const tmpInfo = await new Promise((resolve, reject) => { + tempOpen({ prefix }, (err, info) => { + if (err) { + return reject(err); + } + return resolve(info); + }); + }); + + try { + await new Promise((resolve, reject) => { + fs.write(tmpInfo.fd, contents, err => { + if (err) { + return reject(err); + } + return resolve(); + }); + }); + } finally { + await new Promise((resolve, reject) => { + fs.close(tmpInfo.fd, e => { + if (e) { + return reject(e); + } + return resolve(); + }); + }); + } + return tmpInfo; +}; + export async function connectToChain( basedir, GCI, rpcAddresses, - myAddr, + helperAddr, inbound, chainID, ) { @@ -84,6 +116,38 @@ export async function connectToChain( const helperDir = path.join(basedir, 'ag-cosmos-helper-statedir'); + const readOrDefault = (file, dflt) => + fs.promises + .readFile(file, { encoding: 'utf-8' }) + .catch(e => { + if (e.code === 'ENOENT') { + return dflt; + } + throw e; + }) + .then(str => str.trim()); + + // The helper account may only have the authority to send messages on behalf + // of the client, which has been set up by the client with something like: + // + // ag-cosmos-helper tx authz grant $(cat ag-cosmos-helper-address) \ + // generic --msg-type=/agoric.swingset.MsgDeliverInbound \ + // --from=$(cat cosmos-client-account) + const clientAddr = await readOrDefault( + path.join(basedir, 'cosmos-client-account'), + helperAddr, + ); + + // The helper address may not have a token balance, and instead uses a + // separate fee account, set up with something like: + // + // ag-cosmos-helper tx feegrant grant --period=5 --period-limit=200000urun \ + // $(cat cosmos-fee-account) $(cat ag-cosmos-helper-address) + const feeAccountAddr = await readOrDefault( + path.join(basedir, 'cosmos-fee-account'), + '', + ); + const queued = {}; async function retryRpcAddr(tryOnce) { @@ -246,7 +310,7 @@ export async function connectToChain( queuedHelper( 'getMailbox', 1, // Only one helper running at a time. - ['query', 'swingset', 'mailbox', myAddr, '-ojson'], + ['query', 'swingset', 'mailbox', clientAddr, '-ojson'], // eslint-disable-next-line consistent-return ret => { const { stdout, stderr } = ret; @@ -270,7 +334,7 @@ export async function connectToChain( // Validate that our chain egress exists. await retryRpcAddr(async rpcAddr => { - const args = ['query', 'swingset', 'egress', myAddr]; + const args = ['query', 'swingset', 'egress', clientAddr]; const fullArgs = [ ...args, `--chain-id=${chainID}`, @@ -291,7 +355,9 @@ export async function connectToChain( if (!r.stdout) { console.error(`\ ============= -${chainID} chain does not yet know of address ${myAddr}${adviseEgress(myAddr)} +${chainID} chain does not yet know of address ${clientAddr}${adviseEgress( + clientAddr, + )} ============= `); return undefined; @@ -432,57 +498,65 @@ ${chainID} chain does not yet know of address ${myAddr}${adviseEgress(myAddr)} log( `delivering to chain (trips=${totalDeliveries})`, GCI, - messages[0], - messages[1], + messages, + ack, ); - tmpInfo = await new Promise((resolve, reject) => { - tempOpen({ prefix: 'ag-solo-cosmos-deliver.' }, (err, info) => { - if (err) { - return reject(err); - } - return resolve(info); - }); - }); - - try { - await new Promise((resolve, reject) => { - fs.write( - tmpInfo.fd, - // TODO: remove this JSON.stringify([currentMessages, currentAck]): change - // 'deliverMailboxReq' to have more structure than a single string, and - // have the CLI handle variable args better - JSON.stringify([messages, ack]), - err => { - if (err) { - return reject(err); - } - return resolve(); - }, - ); - }); - } finally { - await new Promise((resolve, reject) => { - fs.close(tmpInfo.fd, e => { - if (e) { - return reject(e); + // TODO: remove this JSON.stringify([currentMessages, currentAck]): change + // 'deliverMailboxReq' to have more structure than a single string, and + // have the CLI handle variable args better + tmpInfo = await makeTempFile( + 'ag-solo-cosmos-deliver.', + JSON.stringify([messages, ack]), + ); + + // Deliver message over file, as it could be big. + let args = ['tx', 'swingset', 'deliver', `@${tmpInfo.path}`]; + if (clientAddr !== helperAddr) { + const genDone = makePromiseKit(); + execFile( + HELPER, + [ + 'tx', + 'swingset', + 'deliver', + '--generate-only', + `--chain-id=${chainID}`, + `--from=${clientAddr}`, + `@${tmpInfo.path}`, + ], + { maxBuffer: MAX_BUFFER_SIZE }, + (error, stdout, stderr) => { + if (error) { + return genDone.reject(error); } - return resolve(); - }); - }); + return genDone.resolve({ stdout, stderr }); + }, + ); + + // Reuse the file to send the constructed transaction. + const { stdout, stderr } = await genDone.promise; + if (stderr) { + throw Error(`Error creating swingset delivery tx; ${stderr}`); + } + await fs.promises.writeFile(tmpInfo.path, stdout); + + args = ['tx', 'authz', 'exec', tmpInfo.path]; } - const args = [ - 'tx', - 'swingset', - 'deliver', + args.push( '--keyring-backend=test', - `@${tmpInfo.path}`, // Deliver message over file, as it could be big. '--gas=auto', - '--gas-adjustment=1.2', + '--gas-adjustment=1.3', + '--broadcast-mode=block', '--from=ag-solo', '--yes', - ]; + ); + + // Use the feeAccount for any fees. + if (feeAccountAddr) { + args.push(`--fee-account=${feeAccountAddr}`); + } // We just try a single delivery per block. const qret = await queuedHelper(