Skip to content

Commit

Permalink
feat: authz example (wip)
Browse files Browse the repository at this point in the history
  • Loading branch information
0xpatrickdev committed Dec 11, 2024
1 parent 6dd673e commit 50ac778
Show file tree
Hide file tree
Showing 8 changed files with 613 additions and 0 deletions.
153 changes: 153 additions & 0 deletions multichain-testing/test/authz-example.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import anyTest from '@endo/ses-ava/prepare-endo.js';
import type { TestFn } from 'ava';
import { makeDoOffer } from '../tools/e2e-tools.js';
import { commonSetup, type SetupContextWithWallets } from './support.js';
import {
createFundedWalletAndClient,
DEFAULT_TIMEOUT_NS,
} from '../tools/ibc-transfer.js';
import { createWallet } from '../tools/wallet.js';
import type { ChainAddress } from '@agoric/orchestration';
import { MsgGrant } from '@agoric/cosmic-proto/cosmos/authz/v1beta1/tx.js';
import { SendAuthorization } from '@agoric/cosmic-proto/cosmos/bank/v1beta1/authz.js';
import { TxBody } from '@agoric/cosmic-proto/cosmos/tx/v1beta1/tx.js';

const test = anyTest as TestFn<SetupContextWithWallets>;

const accounts = ['alice'];

const contractName = 'authzExample';
const contractBuilder =
'../packages/builders/scripts/orchestration/init-authz-example.js';

test.before(async t => {
const { setupTestKeys, ...common } = await commonSetup(t);
const { commonBuilderOpts, deleteTestKeys, startContract } = common;
deleteTestKeys(accounts).catch();
const wallets = await setupTestKeys(accounts);
t.context = { ...common, wallets };
await startContract(contractName, contractBuilder, commonBuilderOpts);
});

test('authz-example', async t => {
const {
provisionSmartWallet,
retryUntilCondition,
useChain,
vstorageClient,
wallets,
} = t.context;

const toChainAddress = (
value: string,
chainName = 'cosmoshub',
): ChainAddress =>
harden({
encoding: 'bech32',
value,
chainId: useChain(chainName).chain.chain_id,
});

// set up "existing account", which will issue MsgGrant
const keplrAccount = await createFundedWalletAndClient(
t.log,
'cosmoshub',
useChain,
);
const keplrAddress = toChainAddress(keplrAccount.address);
t.log('existing account address', keplrAccount.address);

// provision agoric smart wallet to submit offers
// unrelated to wallet above, but we can consider them the same entity
const wdUser = await provisionSmartWallet(wallets['alice'], {
BLD: 100n,
IST: 100n,
});
const doOffer = makeDoOffer(wdUser);

// request an orchestration account
const offerId = `cosmoshub-makeAccount-${Date.now()}`;
doOffer({
id: offerId,
invitationSpec: {
source: 'agoricContract',
instancePath: [contractName],
callPipe: [['makeAccount']],
},
offerArgs: { chainName: 'cosmoshub' },
proposal: {},
});
const currentWalletRecord = await retryUntilCondition(
() =>
vstorageClient.queryData(`published.wallet.${wallets['alice']}.current`),
({ offerToPublicSubscriberPaths }) =>
Object.fromEntries(offerToPublicSubscriberPaths)[offerId],
`${offerId} continuing invitation is in vstorage`,
);
const offerToPublicSubscriberMap = Object.fromEntries(
currentWalletRecord.offerToPublicSubscriberPaths,
);
const address = offerToPublicSubscriberMap[offerId]?.account.split('.').pop();
t.log('Got orch account address:', address);

// generate MsgGrant with SendAuthorization for orch account address and broadcast
const grantMsg = MsgGrant.toProtoMsg({
grantee: address,
granter: keplrAddress.value,
grant: {
// @ts-expect-error the types don't like this, but i think its right
authorization: SendAuthorization.toProtoMsg({
spendLimit: [
{
denom: 'uatom',
amount: '10',
},
],
}),
expiration: {
nanos: 0,
// TODO calculate something more realistic; this is really far in the future
seconds: DEFAULT_TIMEOUT_NS,
},
},
});

const msg = TxBody.encode(
TxBody.fromPartial({
messages: [grantMsg],
// todo, use actual block height
// timeoutHeight: 9999999999n,
}),
).finish();

// FIXME, not working. suspicions:
// 1. MsgExec is not registered in the stargate clients proto registry
// 2. grantMsg, TxBody, are not formed correctly
const res = await keplrAccount.client.broadcastTx(msg);

This comment has been minimized.

Copy link
@0xpatrickdev

0xpatrickdev Dec 11, 2024

Author Member

broadcastTx was giving us issues with MsgTransfer in earlier work. Maybe there's a better story here after #9200.

console.log('res', res);
t.is(res.code, 0);

// create a wallet to be our recipient
const exchangeWallet = await createWallet('cosmos');
const exchangeAddress = toChainAddress(
(await exchangeWallet.getAccounts())[0].address,
);

// submit MsgExec([MsgSend])
await doOffer({
id: `exec-send-${Date.now()}`,
invitationSpec: {
source: 'continuing',
previousOffer: offerId,
invitationMakerName: 'ExecSend',
invitationArgs: [
[{ amount: 10n, denom: 'uatom' }],
exchangeAddress,
keplrAddress,
],
},
proposal: {},
});

// TODO verify exchangeAddress balance increases by 10 uatom
});
67 changes: 67 additions & 0 deletions packages/builders/scripts/orchestration/init-authz-example.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { makeHelpers } from '@agoric/deploy-script-support';
import { startAuthzExample } from '@agoric/orchestration/src/proposals/start-authz-example.js';
import { parseArgs } from 'node:util';

/**
* @import {ParseArgsConfig} from 'node:util'
*/

/** @type {ParseArgsConfig['options']} */
const parserOpts = {
chainInfo: { type: 'string' },
assetInfo: { type: 'string' },
};

/** @type {import('@agoric/deploy-script-support/src/externalTypes.js').CoreEvalBuilder} */
export const defaultProposalBuilder = async (
{ publishRef, install },
options,
) => {
return harden({
sourceSpec: '@agoric/orchestration/src/proposals/start-authz-example.js',
getManifestCall: [
'getManifestForContract',
{
installKeys: {
authzExample: publishRef(
install(
'@agoric/orchestration/src/examples/authz-example.contract.js',
),
),
},
options,
},
],
});
};

/** @type {import('@agoric/deploy-script-support/src/externalTypes.js').DeployScriptFunction} */
export default async (homeP, endowments) => {
const { scriptArgs } = endowments;

const {
values: { chainInfo, assetInfo },
} = parseArgs({
args: scriptArgs,
options: parserOpts,
});

const parseChainInfo = () => {
if (typeof chainInfo !== 'string') return undefined;
return JSON.parse(chainInfo);
};
const parseAssetInfo = () => {
if (typeof assetInfo !== 'string') return undefined;
return JSON.parse(assetInfo);
};
const opts = harden({
chainInfo: parseChainInfo(),
assetInfo: parseAssetInfo(),
});

const { writeCoreEval } = await makeHelpers(homeP, endowments);

await writeCoreEval(startAuthzExample.name, utils =>
defaultProposalBuilder(utils, opts),
);
};
8 changes: 8 additions & 0 deletions packages/cosmic-proto/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@
"types": "./dist/codegen/cosmos/*.d.ts",
"default": "./dist/codegen/cosmos/*.js"
},
"./cosmos/authz/v1beta1/query.js": {
"types": "./dist/codegen/cosmos/authz/v1beta1/query.d.ts",
"default": "./dist/codegen/cosmos/authz/v1beta1/query.js"
},
"./cosmos/authz/v1beta1/tx.js": {
"types": "./dist/codegen/cosmos/authz/v1beta1/tx.d.ts",
"default": "./dist/codegen/cosmos/authz/v1beta1/tx.js"
},
"./cosmos/bank/v1beta1/query.js": {
"types": "./dist/codegen/cosmos/bank/v1beta1/query.d.ts",
"default": "./dist/codegen/cosmos/bank/v1beta1/query.js"
Expand Down
6 changes: 6 additions & 0 deletions packages/orchestration/src/cosmos-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import type {
} from '@agoric/vats/tools/ibc-utils.js';
import type { QueryDelegationTotalRewardsResponse } from '@agoric/cosmic-proto/cosmos/distribution/v1beta1/query.js';
import type { Coin } from '@agoric/cosmic-proto/cosmos/base/v1beta1/coin.js';
import type { Any } from '@agoric/cosmic-proto/google/protobuf/any.js';
import type { AmountArg, ChainAddress, Denom, DenomAmount } from './types.js';
import { PFM_RECEIVER } from './exos/chain-hub.js';

Expand Down Expand Up @@ -299,6 +300,11 @@ export interface LiquidStakingMethods {
liquidStake: (amount: AmountArg) => Promise<void>;
}

export interface AuthzMethods {
/** use `MsgExec` to submit transactions on behalf of another account */
exec: (msgs: Any[], grantee: ChainAddress) => Promise<void>;
}

// TODO support StakingAccountQueries
/** Methods supported only on Agoric chain accounts */
export interface LocalAccountMethods extends StakingAccountActions {
Expand Down
134 changes: 134 additions & 0 deletions packages/orchestration/src/examples/authz-example.contract.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/**
* @file This contract demonstrates using AuthZ to control an existing account
* with an orchestration account.
*
* `MsgExec` is only part of the story, please see
* {@link ../../../../multichain-testing/test/auth-z-example.test.ts} for an
* example of client usage which involves `MsgGrant` txs from the grantee.
*/
import { M } from '@endo/patterns';
import { prepareCombineInvitationMakers } from '../exos/combine-invitation-makers.js';
import { CosmosOrchestrationInvitationMakersI } from '../exos/cosmos-orchestration-account.js';
import { AmountArgShape, ChainAddressShape } from '../typeGuards.js';
import { withOrchestration } from '../utils/start-helper.js';
import * as flows from './authz-example.flows.js';
import { prepareChainHubAdmin } from '../exos/chain-hub-admin.js';

/**
* @import {GuestInterface} from '@agoric/async-flow';
* @import {Zone} from '@agoric/zone';
* @import {OrchestrationTools, OrchestrationPowers} from '../utils/start-helper.js';
* @import {CosmosOrchestrationAccount} from '../exos/cosmos-orchestration-account.js';
* @import {AmountArg, ChainAddress, CosmosChainInfo, Denom, DenomDetail} from '../types.js';
*/

const emptyOfferShape = harden({
// Nothing to give; the funds are deposited offline
give: {},
want: {}, // UNTIL https://github.com/Agoric/agoric-sdk/issues/2230
exit: M.any(),
});

/**
* Orchestration contract to be wrapped by withOrchestration for Zoe.
*
* @param {ZCF} zcf
* @param {OrchestrationPowers & {
* marshaller: Marshaller;
* chainInfo: Record<string, CosmosChainInfo>;
* assetInfo: [Denom, DenomDetail & { brandKey?: string }][];
* }} privateArgs
* @param {Zone} zone
* @param {OrchestrationTools} tools
*/
const contract = async (
zcf,
privateArgs,
zone,
{ orchestrateAll, zoeTools, chainHub },
) => {
const AuthzExampleInvitationMakersI = M.interface(
'AuthzExampleInvitationMakersI',
{
ExecSend: M.call(
M.arrayOf(AmountArgShape),
ChainAddressShape,
ChainAddressShape,
).returns(M.promise()),
},
);

/** @type {any} XXX async membrane */
const makeExtraInvitationMaker = zone.exoClass(
'AuthzExampleInvitationMakers',
AuthzExampleInvitationMakersI,
/**
* @param {GuestInterface<CosmosOrchestrationAccount>} account
* @param {string} chainName
*/
(account, chainName) => {
return { account, chainName };
},
{
/**
* @param {AmountArg[]} amounts
* @param {ChainAddress} destination
* @param {ChainAddress} grantee
*/
ExecSend(amounts, destination, grantee) {
const { account } = this.state;

return zcf.makeInvitation(
seat =>
orchFns.execSend(account, seat, {
amounts,
destination,
grantee,
}),
'Exec Send',
undefined,
emptyOfferShape,
);
},
},
);

/** @type {any} XXX async membrane */
const makeCombineInvitationMakers = prepareCombineInvitationMakers(
zone,
CosmosOrchestrationInvitationMakersI,
AuthzExampleInvitationMakersI,
);

const orchFns = orchestrateAll(flows, {
chainHub,
makeCombineInvitationMakers,
makeExtraInvitationMaker,
flows,
zoeTools,
});

/**
* Provide invitations to contract deployer for registering assets and chains
* in the local ChainHub for this contract.
*/
const creatorFacet = prepareChainHubAdmin(zone, chainHub);

const publicFacet = zone.exo('publicFacet', undefined, {
makeAccount() {
return zcf.makeInvitation(
orchFns.makeAccount,
'Make an Orchestration account',
undefined,
emptyOfferShape,
);
},
});

return harden({ publicFacet, creatorFacet });
};

export const start = withOrchestration(contract);
harden(start);

/** @typedef {typeof start} AuthzExampleSF */
Loading

0 comments on commit 50ac778

Please sign in to comment.