-
Notifications
You must be signed in to change notification settings - Fork 3
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
feat: support offer signing with keplr #28
Changes from all commits
cdad864
604a129
bc2b2e5
3a8b5cc
baef932
1942589
a92a05a
87a68d8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
import { Far, makeMarshal } from '@endo/marshal'; | ||
|
||
const makeTranslationTable = ( | ||
makeSlot: (val: unknown, size: number) => unknown, | ||
makeVal: (slot: unknown, iface: string | undefined) => unknown, | ||
) => { | ||
const valToSlot = new Map(); | ||
const slotToVal = new Map(); | ||
|
||
const convertValToSlot = (val: unknown) => { | ||
if (valToSlot.has(val)) return valToSlot.get(val); | ||
const slot = makeSlot(val, valToSlot.size); | ||
valToSlot.set(val, slot); | ||
slotToVal.set(slot, val); | ||
return slot; | ||
}; | ||
|
||
const convertSlotToVal = (slot: unknown, iface: string | undefined) => { | ||
if (slot === null) return makeVal(slot, iface); | ||
if (slotToVal.has(slot)) return slotToVal.get(slot); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. hm. a slot of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you mean we should just do something like:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I can't see clearly why it's bad for all null slots to resolve to the same presence There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if on-chain code uses the read-only marshaller to marshal distinct unpublished remotables X and Y, they both come across with The down side of treating There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
something like that, yes. But specifically: it should There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. Yea, I was thinking along the lines of "null slot means null value on-chain", so thought it was okay if they were treated as There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I observe(d) null slots in mainnet but not on devnet, so this may already be addressed. When i query The offending data blob:
Happy to create an issue in the main repo if needed. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's expected for vstorage to have null slots because not all objects referenced are necessarily public. For instance, the final slot in the quote example is for a quote payment, which must not be published. |
||
const val = makeVal(slot, iface); | ||
valToSlot.set(val, slot); | ||
slotToVal.set(slot, val); | ||
return val; | ||
}; | ||
|
||
return harden({ convertValToSlot, convertSlotToVal }); | ||
}; | ||
|
||
const synthesizeRemotable = (_slot: unknown, iface: string | undefined) => | ||
Far((iface ?? '').replace(/^Alleged: /, ''), {}); | ||
|
||
const { convertValToSlot, convertSlotToVal } = makeTranslationTable(slot => { | ||
throw new Error(`unknown id: ${slot}`); | ||
}, synthesizeRemotable); | ||
|
||
export const makeClientMarshaller = () => | ||
makeMarshal(convertValToSlot, convertSlotToVal, { | ||
serializeBodyFormat: 'smallcaps', | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
/** @typedef {import('@keplr-wallet/types').Bech32Config} Bech32Config */ | ||
/** @typedef {import('@keplr-wallet/types').FeeCurrency} FeeCurrency */ | ||
|
||
/** @type {FeeCurrency} */ | ||
export const stakeCurrency = { | ||
coinDenom: 'BLD', | ||
coinMinimalDenom: 'ubld', | ||
coinDecimals: 6, | ||
coinGeckoId: undefined, | ||
gasPriceStep: { | ||
low: 0, | ||
average: 0, | ||
high: 0, | ||
}, | ||
}; | ||
|
||
/** @type {FeeCurrency} */ | ||
export const stableCurrency = { | ||
coinDenom: 'IST', | ||
coinMinimalDenom: 'uist', | ||
coinDecimals: 6, | ||
coinGeckoId: undefined, | ||
gasPriceStep: { | ||
low: 0, | ||
average: 0, | ||
high: 0, | ||
}, | ||
}; | ||
|
||
/** @type {Bech32Config} */ | ||
export const bech32Config = { | ||
bech32PrefixAccAddr: 'agoric', | ||
bech32PrefixAccPub: 'agoricpub', | ||
bech32PrefixValAddr: 'agoricvaloper', | ||
bech32PrefixValPub: 'agoricvaloperpub', | ||
bech32PrefixConsAddr: 'agoricvalcons', | ||
bech32PrefixConsPub: 'agoricvalconspub', | ||
}; |
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This file was taken from wallet-app and trimmed down a bit to remove the non-spend-action stuff. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,156 @@ | ||
import { fromBech32, toBech32, fromBase64, toBase64 } from '@cosmjs/encoding'; | ||
import { Registry } from '@cosmjs/proto-signing'; | ||
import { | ||
AminoTypes, | ||
defaultRegistryTypes, | ||
assertIsDeliverTxSuccess, | ||
createBankAminoConverters, | ||
createAuthzAminoConverters, | ||
} from '@cosmjs/stargate'; | ||
import { MsgWalletSpendAction } from '@agoric/cosmic-proto/swingset/msgs.js'; | ||
import { stableCurrency, bech32Config } from './chainInfo.js'; | ||
import { Errors } from '../errors.js'; | ||
|
||
/** @typedef {import("@cosmjs/proto-signing").EncodeObject} EncodeObject */ | ||
/** @typedef {import("@cosmjs/stargate").AminoConverters} AminoConverters */ | ||
/** @typedef {import("@cosmjs/stargate").StdFee} StdFee */ | ||
/** @typedef {import('@keplr-wallet/types').ChainInfo} ChainInfo */ | ||
/** @typedef {import('@keplr-wallet/types').Keplr} Keplr */ | ||
|
||
/** | ||
* @param {string} address | ||
* @returns {Uint8Array} | ||
*/ | ||
const toAccAddress = address => { | ||
return fromBech32(address).data; | ||
}; | ||
|
||
/** | ||
* `/agoric.swingset.XXX` matches package agoric.swingset in swingset/msgs.proto | ||
* aminoType taken from Type() in golang/cosmos/x/swingset/types/msgs.go | ||
*/ | ||
const SwingsetMsgs = { | ||
MsgWalletSpendAction: { | ||
typeUrl: '/agoric.swingset.MsgWalletSpendAction', | ||
aminoType: 'swingset/WalletSpendAction', | ||
}, | ||
}; | ||
|
||
/** @typedef {{owner: string, spendAction: string}} WalletSpendAction */ | ||
|
||
const SwingsetRegistry = new Registry([ | ||
...defaultRegistryTypes, | ||
// XXX should this list be "upstreamed" to @agoric/cosmic-proto? | ||
[SwingsetMsgs.MsgWalletSpendAction.typeUrl, MsgWalletSpendAction], | ||
]); | ||
|
||
/** | ||
* @returns {StdFee} | ||
*/ | ||
const zeroFee = () => { | ||
const { coinMinimalDenom: denom } = stableCurrency; | ||
const fee = { | ||
amount: [{ amount: '0', denom }], | ||
gas: '300000', // TODO: estimate gas? | ||
}; | ||
return fee; | ||
}; | ||
|
||
/** | ||
* @type {AminoConverters} | ||
*/ | ||
const SwingsetConverters = { | ||
[SwingsetMsgs.MsgWalletSpendAction.typeUrl]: { | ||
aminoType: SwingsetMsgs.MsgWalletSpendAction.aminoType, | ||
toAmino: ({ spendAction, owner }) => ({ | ||
spend_action: spendAction, | ||
owner: toBech32(bech32Config.bech32PrefixAccAddr, fromBase64(owner)), | ||
}), | ||
fromAmino: ({ spend_action: spendAction, owner }) => ({ | ||
spendAction, | ||
owner: toBase64(toAccAddress(owner)), | ||
}), | ||
}, | ||
}; | ||
|
||
/** | ||
* Use Keplr to sign offers and delegate object messaging to local storage key. | ||
* @param {string} chainId | ||
* @param {string} rpc | ||
* @param {Keplr} keplr | ||
* @param {typeof import('@cosmjs/stargate').SigningStargateClient.connectWithSigner} connectWithSigner | ||
Comment on lines
+80
to
+81
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Glad to see the use of explicit authority is still here. :) |
||
* Ref: https://docs.keplr.app/api/ | ||
*/ | ||
export const makeInteractiveSigner = async ( | ||
chainId, | ||
rpc, | ||
keplr, | ||
connectWithSigner, | ||
) => { | ||
await null; | ||
try { | ||
await keplr.enable(chainId); | ||
} catch { | ||
throw Error(Errors.enableKeplr); | ||
} | ||
|
||
const key = await keplr.getKey(chainId); | ||
|
||
// Until we have SIGN_MODE_TEXTUAL, | ||
// Use Amino because Direct results in ugly protobuf in the keplr UI. | ||
const offlineSigner = await keplr.getOfflineSignerOnlyAmino(chainId); | ||
console.debug('InteractiveSigner', { offlineSigner }); | ||
|
||
// Currently, Keplr extension manages only one address/public key pair. | ||
const [account] = await offlineSigner.getAccounts(); | ||
const { address } = account; | ||
|
||
const converters = { | ||
...SwingsetConverters, | ||
...createBankAminoConverters(), | ||
...createAuthzAminoConverters(), | ||
}; | ||
const signingClient = await connectWithSigner(rpc, offlineSigner, { | ||
aminoTypes: new AminoTypes(converters), | ||
registry: SwingsetRegistry, | ||
}); | ||
console.debug('InteractiveSigner', { signingClient }); | ||
|
||
const fee = zeroFee(); | ||
|
||
return harden({ | ||
address, // TODO: address can change | ||
isNanoLedger: key.isNanoLedger, | ||
/** | ||
* Sign and broadcast WalletSpendAction | ||
* | ||
* @param {string} spendAction marshaled offer | ||
* @throws if account does not exist on chain, user cancels, | ||
* RPC connection fails, RPC service fails to broadcast ( | ||
* for example, if signature verification fails) | ||
*/ | ||
submitSpendAction: async spendAction => { | ||
const { accountNumber, sequence } = await signingClient.getSequence( | ||
address, | ||
); | ||
console.debug({ accountNumber, sequence }); | ||
|
||
const act1 = { | ||
typeUrl: SwingsetMsgs.MsgWalletSpendAction.typeUrl, | ||
value: { | ||
owner: toBase64(toAccAddress(address)), | ||
spendAction, | ||
}, | ||
}; | ||
|
||
const msgs = [act1]; | ||
console.debug('sign spend action', { address, msgs, fee }); | ||
|
||
const tx = await signingClient.signAndBroadcast(address, msgs, fee); | ||
console.debug('spend action result tx', tx); | ||
assertIsDeliverTxSuccess(tx); | ||
|
||
return tx; | ||
}, | ||
}); | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See "marshal.ts" for an example that should work with null slots.