From b898b97a4d1f59784f3248f1cfce68d33e25933e Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Sat, 2 Mar 2024 19:49:59 -0700 Subject: [PATCH 1/6] chore: vendor smart-wallet types --- .../src/@types/smart-wallet/invitations.d.ts | 45 ++++ .../@types/smart-wallet/marshal-contexts.d.ts | 88 ++++++++ .../src/@types/smart-wallet/offerWatcher.d.ts | 42 ++++ .../src/@types/smart-wallet/offers.d.ts | 27 +++ .../src/@types/smart-wallet/smartWallet.d.ts | 192 ++++++++++++++++++ .../src/@types/smart-wallet/typeGuards.d.ts | 4 + .../src/@types/smart-wallet/types.d.ts | 85 ++++++++ .../src/@types/smart-wallet/utils.d.ts | 26 +++ .../@types/smart-wallet/walletFactory.d.ts | 153 ++++++++++++++ 9 files changed, 662 insertions(+) create mode 100644 packages/ag-trade/src/@types/smart-wallet/invitations.d.ts create mode 100644 packages/ag-trade/src/@types/smart-wallet/marshal-contexts.d.ts create mode 100644 packages/ag-trade/src/@types/smart-wallet/offerWatcher.d.ts create mode 100644 packages/ag-trade/src/@types/smart-wallet/offers.d.ts create mode 100644 packages/ag-trade/src/@types/smart-wallet/smartWallet.d.ts create mode 100644 packages/ag-trade/src/@types/smart-wallet/typeGuards.d.ts create mode 100644 packages/ag-trade/src/@types/smart-wallet/types.d.ts create mode 100644 packages/ag-trade/src/@types/smart-wallet/utils.d.ts create mode 100644 packages/ag-trade/src/@types/smart-wallet/walletFactory.d.ts diff --git a/packages/ag-trade/src/@types/smart-wallet/invitations.d.ts b/packages/ag-trade/src/@types/smart-wallet/invitations.d.ts new file mode 100644 index 0000000..7dc0ac8 --- /dev/null +++ b/packages/ag-trade/src/@types/smart-wallet/invitations.d.ts @@ -0,0 +1,45 @@ +export function makeInvitationsHelper(zoe: ERef, agoricNames: ERef, invitationBrand: Brand<'set'>, invitationsPurse: Purse<'set'>, getInvitationContinuation: (fromOfferId: string) => import('./types.js').InvitationMakers): (spec: InvitationSpec) => ERef; +/** + * Specify how to produce an invitation. See each type in the union for details. + */ +export type InvitationSpec = AgoricContractInvitationSpec | ContractInvitationSpec | PurseInvitationSpec | ContinuingInvitationSpec; +/** + * source of invitation is a chain of calls starting with an agoricName + * - the start of the pipe is a lookup of instancePath within agoricNames + * - each entry in the callPipe executes a call on the preceding result + * - the end of the pipe is expected to return an Invitation + */ +export type AgoricContractInvitationSpec = { + source: 'agoricContract'; + instancePath: string[]; + callPipe: Array<[methodName: string, methodArgs?: any[]]>; +}; +/** + * source is a contract (in which case this takes an Instance to look up in zoe) + */ +export type ContractInvitationSpec = { + source: 'contract'; + instance: Instance; + publicInvitationMaker: string; + invitationArgs?: any[]; +}; +/** + * the invitation is already in your Zoe "invitation" purse so we need to query it + * - use the find/query invitation by kvs thing + */ +export type PurseInvitationSpec = { + source: 'purse'; + instance: Instance; + description: string; +}; +/** + * continuing invitation in which the offer result from a previous invitation had an `invitationMakers` property + */ +export type ContinuingInvitationSpec = { + source: 'continuing'; + previousOffer: import('./offers.js').OfferId; + invitationMakerName: string; + invitationArgs?: any[]; +}; +export type InvitationsPurseQuery = Pick; +//# sourceMappingURL=invitations.d.ts.map \ No newline at end of file diff --git a/packages/ag-trade/src/@types/smart-wallet/marshal-contexts.d.ts b/packages/ag-trade/src/@types/smart-wallet/marshal-contexts.d.ts new file mode 100644 index 0000000..08bb93b --- /dev/null +++ b/packages/ag-trade/src/@types/smart-wallet/marshal-contexts.d.ts @@ -0,0 +1,88 @@ +export function makeExportContext(): { + toCapData: import("@endo/marshal/src/marshal").ToCapData; + fromCapData: import("@endo/marshal/src/marshal").FromCapData; + serialize: import("@endo/marshal/src/marshal").ToCapData; + unserialize: import("@endo/marshal/src/marshal").FromCapData; + savePurseActions: (val: Purse) => void; + savePaymentActions: (val: Payment) => void; + /** + * @param {number} id + * @param {Purse} purse + */ + initPurseId: (id: number, purse: Purse) => void; + purseEntries: (keyPatt?: any, valuePatt?: any) => Iterable<[number, Purse]>; + /** + * @param {BoardId} id + * @param {unknown} val + */ + initBoardId: (id: BoardId, val: unknown) => void; + /** + * @param {BoardId} id + * @param {unknown} val + */ + ensureBoardId: (id: BoardId, val: unknown) => void; +}; +export function makeImportContext(makePresence?: ((iface: string) => unknown) | undefined): { + /** + * @param {BoardId} id + * @param {unknown} val + */ + initBoardId: (id: BoardId, val: unknown) => void; + /** + * @param {BoardId} id + * @param {unknown} val + */ + ensureBoardId: (id: BoardId, val: unknown) => void; + fromMyWallet: { + toCapData: import("@endo/marshal/src/marshal").ToCapData; + fromCapData: import("@endo/marshal/src/marshal").FromCapData; + serialize: import("@endo/marshal/src/marshal").ToCapData; + unserialize: import("@endo/marshal/src/marshal").FromCapData; + } & import("@endo/eventual-send").RemotableBrand<{}, { + toCapData: import("@endo/marshal/src/marshal").ToCapData; + fromCapData: import("@endo/marshal/src/marshal").FromCapData; + serialize: import("@endo/marshal/src/marshal").ToCapData; + unserialize: import("@endo/marshal/src/marshal").FromCapData; + }>; + fromBoard: { + toCapData: import("@endo/marshal/src/marshal").ToCapData; + fromCapData: import("@endo/marshal/src/marshal").FromCapData; + serialize: import("@endo/marshal/src/marshal").ToCapData; + unserialize: import("@endo/marshal/src/marshal").FromCapData; + } & import("@endo/eventual-send").RemotableBrand<{}, { + toCapData: import("@endo/marshal/src/marshal").ToCapData; + fromCapData: import("@endo/marshal/src/marshal").FromCapData; + serialize: import("@endo/marshal/src/marshal").ToCapData; + unserialize: import("@endo/marshal/src/marshal").FromCapData; + }>; +}; +export function makeLoggingPresence(iface: string, log: (parts: unknown[]) => void): any; +export type BoardId = import('@agoric/vats/src/lib-board.js').BoardId; +/** + * + */ +export type WalletSlot>> = `${string & keyof T}:${Digits}`; +/** + * + */ +export type KindSlot = `${K}:${Digits}`; +/** + * + */ +export type MixedSlot>> = WalletSlot | BoardId; +/** + * - 1 or more digits. + * NOTE: the typescript definition here is more restrictive than + * actual usage. + */ +export type Digits = `1` | `12` | `123`; +/** + * + */ +export type IdTable = { + bySlot: MapStore; + byVal: MapStore; +}; +export type ExportContext = ReturnType; +export type ImportContext = ReturnType; +//# sourceMappingURL=marshal-contexts.d.ts.map \ No newline at end of file diff --git a/packages/ag-trade/src/@types/smart-wallet/offerWatcher.d.ts b/packages/ag-trade/src/@types/smart-wallet/offerWatcher.d.ts new file mode 100644 index 0000000..499f9de --- /dev/null +++ b/packages/ag-trade/src/@types/smart-wallet/offerWatcher.d.ts @@ -0,0 +1,42 @@ +export function watchOfferOutcomes(watchers: OutcomeWatchers, seat: UserSeat): Promise<[unknown, 0 | 1, PaymentPKeywordRecord]>; +export function prepareOfferWatcher(baggage: import('@agoric/vat-data').Baggage): (walletHelper: any, deposit: any, offerSpec: import("./offers.js").OfferSpec, address: string, invitationAmount: Amount<"set">, seatRef: UserSeat) => import("@endo/exo/src/exo-makers.js").GuardedKit<{ + helper: { + /** + * @param {Record} offerStatusUpdates + */ + updateStatus(offerStatusUpdates: Record): void; + onNewContinuingOffer(offerId: any, invitationAmount: any, invitationMakers: any, publicSubscribers: any): void; + /** @param {unknown} result */ + publishResult(result: unknown): void; + /** + * Called when the offer result promise rejects. The other two watchers + * are waiting for particular values out of Zoe but they settle at the same time + * and don't need their own error handling. + * @param {Error} err + */ + handleError(err: Error): void; + }; + /** @type {OutcomeWatchers['paymentWatcher']} */ + paymentWatcher: OutcomeWatchers['paymentWatcher']; + /** @type {OutcomeWatchers['resultWatcher']} */ + resultWatcher: OutcomeWatchers['resultWatcher']; + /** @type {OutcomeWatchers['numWantsWatcher']} */ + numWantsWatcher: OutcomeWatchers['numWantsWatcher']; +}>; +export type OfferStatus = import('./offers.js').OfferSpec & { + error?: string; + numWantsSatisfied?: number; + result?: unknown | typeof import('./offers.js').UNPUBLISHED_RESULT; + payouts?: AmountKeywordRecord; +}; +/** + * = import('@agoric/swingset-liveslots').PromiseWatcher; +export type OutcomeWatchers = { + resultWatcher: OfferPromiseWatcher; + numWantsWatcher: OfferPromiseWatcher; + paymentWatcher: OfferPromiseWatcher; +}; +export type MakeOfferWatcher = ReturnType; +//# sourceMappingURL=offerWatcher.d.ts.map \ No newline at end of file diff --git a/packages/ag-trade/src/@types/smart-wallet/offers.d.ts b/packages/ag-trade/src/@types/smart-wallet/offers.d.ts new file mode 100644 index 0000000..3c69c4d --- /dev/null +++ b/packages/ag-trade/src/@types/smart-wallet/offers.d.ts @@ -0,0 +1,27 @@ +/** + * @typedef {number | string} OfferId + */ +/** + * @typedef {{ + * id: OfferId, + * invitationSpec: import('./invitations.js').InvitationSpec, + * proposal: Proposal, + * offerArgs?: unknown + * }} OfferSpec + */ +/** Value for "result" field when the result can't be published */ +export const UNPUBLISHED_RESULT: "UNPUBLISHED"; +export type OfferId = number | string; +export type OfferSpec = { + id: OfferId; + invitationSpec: import('./invitations.js').InvitationSpec; + proposal: Proposal; + offerArgs?: unknown; +}; +export type OfferStatus = import('./offers.js').OfferSpec & { + error?: string; + numWantsSatisfied?: number; + result?: unknown | typeof UNPUBLISHED_RESULT; + payouts?: AmountKeywordRecord; +}; +//# sourceMappingURL=offers.d.ts.map \ No newline at end of file diff --git a/packages/ag-trade/src/@types/smart-wallet/smartWallet.d.ts b/packages/ag-trade/src/@types/smart-wallet/smartWallet.d.ts new file mode 100644 index 0000000..c4fd288 --- /dev/null +++ b/packages/ag-trade/src/@types/smart-wallet/smartWallet.d.ts @@ -0,0 +1,192 @@ +export const BRAND_TO_PURSES_KEY: "brandToPurses"; +export function prepareSmartWallet(baggage: import('@agoric/vat-data').Baggage, shared: SharedParams): (uniqueWithoutChildNodes: Omit & { + walletStorageNode: ERef; +}) => Promise} actionCapData of type BridgeAction + * @param {boolean} [canSpend] + * @returns {Promise} + */ + handleBridgeAction(actionCapData: import('@endo/marshal').CapData, canSpend?: boolean | undefined): Promise; + getDepositFacet(): import("@endo/exo/src/exo-makers.js").Guarded<{ + /** + * Put the assets from the payment into the appropriate purse. + * + * If the purse doesn't exist, we hold the payment in durable storage. + * + * @param {Payment} payment + * @returns {Promise} + * @throws if there's not yet a purse, though the payment is held to try again when there is + */ + receive(payment: Payment): Promise; + }>; + getOffersFacet(): import("@endo/exo/src/exo-makers.js").Guarded<{ + /** + * Take an offer description provided in capData, augment it with payments and call zoe.offer() + * + * @param {OfferSpec} offerSpec + * @returns {Promise} after the offer has been both seated and exited by Zoe. + * @throws if any parts of the offer can be determined synchronously to be invalid + */ + executeOffer(offerSpec: OfferSpec): Promise; + /** + * Take an offer's id, look up its seat, try to exit. + * + * @param {OfferId} offerId + * @returns {Promise} + * @throws if the seat can't be found or E(seatRef).tryExit() fails. + */ + tryExitOffer(offerId: OfferId): Promise; + }>; + /** @deprecated use getPublicTopics */ + getCurrentSubscriber(): Subscriber; + /** @deprecated use getPublicTopics */ + getUpdatesSubscriber(): Subscriber; + getPublicTopics(): { + current: { + description: string; + subscriber: Subscriber; + storagePath: Promise; + }; + updates: { + description: string; + subscriber: Subscriber; + storagePath: Promise; + }; + }; + /** + * To be called once ever per wallet. + * + * @param {object} key + */ + repairWalletForIncarnation2(key: object): void; +}>>; +export type OfferId = number | string; +export type OfferSpec = { + id: OfferId; + invitationSpec: import('./invitations').InvitationSpec; + proposal: Proposal; + offerArgs?: unknown; +}; +export type ExecutorPowers = { + logger: { + info: (...args: any[]) => void; + error: (...args: any[]) => void; + }; + makeOfferWatcher: import('./offerWatcher.js').MakeOfferWatcher; + invitationFromSpec: ERef; +}; +export type ExecuteOfferAction = { + method: 'executeOffer'; + offer: OfferSpec; +}; +export type TryExitOfferAction = { + method: 'tryExitOffer'; + offerId: OfferId; +}; +export type BridgeAction = ExecuteOfferAction | TryExitOfferAction; +/** + * Purses is an array to support a future requirement of multiple purses per brand. + * + * Each map is encoded as an array of entries because a Map doesn't serialize directly. + * We also considered having a vstorage key for each offer but for now are sticking with this design. + * + * Cons + * - Reserializes previously written results when a new result is added + * - Optimizes reads though writes are on-chain (~100 machines) and reads are off-chain (to 1 machine) + * + * Pros + * - Reading all offer results happens much more (>100) often than storing a new offer result + * - Reserialization and writes are paid in execution gas, whereas reads are not + * + * This design should be revisited if ever batch querying across vstorage keys become cheaper or reads be paid. + */ +export type CurrentWalletRecord = { + purses: Array<{ + brand: Brand; + balance: Amount; + }>; + offerToUsedInvitation: Array<[offerId: string, usedInvitation: Amount]>; + offerToPublicSubscriberPaths: [offerId: string, publicTopics: { + [subscriberName: string]: string; + }][]; + liveOffers: Array<[OfferId, import('./offers.js').OfferStatus]>; +}; +/** + * Record of an update to the state of this wallet. + * + * Client is responsible for coalescing updates into a current state. See `coalesceUpdates` utility. + * + * The reason for this burden on the client is that publishing + * the full history of offers with each change is untenable. + * + * `balance` update supports forward-compatibility for more than one purse per + * brand. An additional key will be needed to disambiguate. For now the brand in + * the amount suffices. + */ +export type UpdateRecord = { + updated: 'offerStatus'; + status: import('./offers.js').OfferStatus; +} | { + updated: 'balance'; + currentAmount: Amount; +} | { + updated: 'walletAction'; + status: { + error: string; + }; +}; +/** + * For use by clients to describe brands to users. Includes `displayInfo` to save a remote call. + */ +export type BrandDescriptor = { + brand: Brand; + displayInfo: DisplayInfo; + issuer: Issuer; + petname: import('./types.js').Petname; +}; +export type UniqueParams = { + address: string; + bank: ERef; + currentStorageNode: StorageNode; + invitationPurse: Purse<'set'>; + walletStorageNode: StorageNode; +}; +export type BrandDescriptorRegistry = Pick, 'has' | 'get' | 'values'>; +export type SharedParams = { + agoricNames: ERef; + registry: BrandDescriptorRegistry; + invitationIssuer: Issuer<'set'>; + invitationBrand: Brand<'set'>; + invitationDisplayInfo: DisplayInfo; + publicMarshaller: Marshaller; + zoe: ERef; + secretWalletFactoryKey: any; +}; +/** + * - `brandPurses` is precious and closely held. defined as late as possible to reduce its scope. + * - `offerToInvitationMakers` is precious and closely held. + * - `offerToPublicSubscriberPaths` is precious and closely held. + * - `purseBalances` is a cache of what we've received from purses. Held so we can publish all balances on change. + */ +export type State = ImmutableState & MutableState; +export type ImmutableState = Readonly>; + offerToInvitationMakers: MapStore; + offerToPublicSubscriberPaths: MapStore>; + offerToUsedInvitation: MapStore>; + purseBalances: MapStore; + updateRecorderKit: import('@agoric/zoe/src/contractSupport/recorder.js').RecorderKit; + currentRecorderKit: import('@agoric/zoe/src/contractSupport/recorder.js').RecorderKit; + liveOffers: MapStore; + liveOfferSeats: MapStore>; + liveOfferPayments: MapStore>; +}>; +export type PurseRecord = BrandDescriptor & { + purse: Purse; +}; +export type MutableState = {}; +export type SmartWallet = Awaited>>; +//# sourceMappingURL=smartWallet.d.ts.map \ No newline at end of file diff --git a/packages/ag-trade/src/@types/smart-wallet/typeGuards.d.ts b/packages/ag-trade/src/@types/smart-wallet/typeGuards.d.ts new file mode 100644 index 0000000..dc862b3 --- /dev/null +++ b/packages/ag-trade/src/@types/smart-wallet/typeGuards.d.ts @@ -0,0 +1,4 @@ +export namespace shape { + let WalletBridgeMsg: import("@endo/patterns").Matcher; +} +//# sourceMappingURL=typeGuards.d.ts.map \ No newline at end of file diff --git a/packages/ag-trade/src/@types/smart-wallet/types.d.ts b/packages/ag-trade/src/@types/smart-wallet/types.d.ts new file mode 100644 index 0000000..f12822d --- /dev/null +++ b/packages/ag-trade/src/@types/smart-wallet/types.d.ts @@ -0,0 +1,85 @@ +/** + * @file Some types for smart-wallet contract + * + * Similar to types.js but in TypeScript syntax because some types here need it. + * Downside is it can't reference any ambient types, which most of agoric-sdk type are presently. + */ + +import type { ERef } from '@endo/far'; +import type { CapData } from '@endo/marshal'; +import type { agoric } from '@agoric/cosmic-proto'; +import type { AgoricNamesRemotes } from '@agoric/vats/tools/board-utils.js'; +import type { OfferSpec } from './offers.js'; + +declare const CapDataShape: unique symbol; + +/** + * A petname can either be a plain string or a path for which the first element + * is a petname for the origin, and the rest of the elements are a snapshot of + * the names that were first given by that origin. We are migrating away from + * using plain strings, for consistency. + */ +export type Petname = string | string[]; + +export type InvitationMakers = Record< + string, + (...args: any[]) => Promise +>; + +export type PublicSubscribers = Record>; + +export type Cell = { + get: () => T; + set(val: T): void; +}; + +export type BridgeActionCapData = WalletCapData< + import('./smartWallet.js').BridgeAction +>; + +/** + * Defined by walletAction struct in msg_server.go + * + * @see {agoric.swingset.MsgWalletAction} and walletSpendAction in msg_server.go + */ +export type WalletActionMsg = { + type: 'WALLET_ACTION'; + /** base64 of Uint8Array of bech32 data */ + owner: string; + /** JSON of BridgeActionCapData */ + action: string; + blockHeight: unknown; // int64 + blockTime: unknown; // int64 +}; + +/** + * Defined by walletSpendAction struct in msg_server.go + * + * @see {agoric.swingset.MsgWalletSpendAction} and walletSpendAction in msg_server.go + */ +export type WalletSpendActionMsg = { + type: 'WALLET_SPEND_ACTION'; + /** base64 of Uint8Array of bech32 data */ + owner: string; + /** JSON of BridgeActionCapData */ + spendAction: string; + blockHeight: unknown; // int64 + blockTime: unknown; // int64 +}; + +/** + * Messages transmitted over Cosmos chain, cryptographically verifying that the + * message came from the 'owner'. + * + * The two wallet actions are distinguished by whether the user had to confirm + * the sending of the message (as is the case for WALLET_SPEND_ACTION). + */ +export type WalletBridgeMsg = WalletActionMsg | WalletSpendActionMsg; + +/** + * Used for clientSupport helpers + */ +export type OfferMaker = ( + agoricNames: AgoricNamesRemotes, + ...rest: any[] +) => OfferSpec; diff --git a/packages/ag-trade/src/@types/smart-wallet/utils.d.ts b/packages/ag-trade/src/@types/smart-wallet/utils.d.ts new file mode 100644 index 0000000..27ffc0f --- /dev/null +++ b/packages/ag-trade/src/@types/smart-wallet/utils.d.ts @@ -0,0 +1,26 @@ +export const NO_SMART_WALLET_ERROR: "no smart wallet"; +export function makeWalletStateCoalescer(invitationBrand?: Brand<"set"> | undefined): { + state: { + invitationsReceived: Map; + offerStatuses: Map; + balances: Map, Amount>; + }; + update: (updateRecord: import('./smartWallet.js').UpdateRecord | {}) => void; +}; +export function coalesceUpdates(updates: ERef>, invitationBrand?: Brand<"set"> | undefined): { + invitationsReceived: Map; + offerStatuses: Map; + balances: Map, Amount>; +}; +export function assertHasData(follower: import('@agoric/casting').Follower): Promise; +export function objectMapStoragePath(subscribers?: import("./types.js").PublicSubscribers | import("@agoric/zoe/src/contractSupport/topics.js").TopicsRecord | undefined): ERef> | null; +export type CoalescedWalletState = ReturnType['state']; +//# sourceMappingURL=utils.d.ts.map \ No newline at end of file diff --git a/packages/ag-trade/src/@types/smart-wallet/walletFactory.d.ts b/packages/ag-trade/src/@types/smart-wallet/walletFactory.d.ts new file mode 100644 index 0000000..4eb5739 --- /dev/null +++ b/packages/ag-trade/src/@types/smart-wallet/walletFactory.d.ts @@ -0,0 +1,153 @@ +export namespace customTermsShape { + let agoricNames: any; + let board: any; + let assetPublisher: any; +} +export const privateArgsShape: import("@endo/patterns").Matcher; +export function publishDepositFacet(address: string, wallet: import('./smartWallet.js').SmartWallet, namesByAddressAdmin: ERef): Promise; +export function makeAssetRegistry(assetPublisher: AssetPublisher): { + /** @param {Brand} brand */ + has: (brand: Brand) => boolean; + /** @param {Brand} brand */ + get: (brand: Brand) => { + brand: Brand; + displayInfo: DisplayInfo; + issuer: Issuer; + petname: import('./types.js').Petname; + }; + values: () => Iterable<{ + brand: Brand; + displayInfo: DisplayInfo; + issuer: Issuer; + petname: import('./types.js').Petname; + }>; +}; +export function prepare(zcf: ZCF, privateArgs: { + storageNode: ERef; + walletBridgeManager?: ERef | undefined; + walletReviver?: ERef | undefined; +}, baggage: import('@agoric/vat-data').Baggage): Promise<{ + creatorFacet: import("@endo/exo/src/exo-makers.js").Guarded<{ + /** + * @param {string} address + * @param {ERef} bank + * @param {ERef} namesByAddressAdmin + * @returns {Promise<[wallet: import('./smartWallet.js').SmartWallet, isNew: boolean]>} wallet + * along with a flag to distinguish between looking up an existing wallet + * and creating a new one. + */ + provideSmartWallet(address: string, bank: ERef, namesByAddressAdmin: ERef): Promise<[wallet: { + handleBridgeAction(actionCapData: import("@endo/marshal").CapData, canSpend?: boolean | undefined): Promise; + getDepositFacet(): import("@endo/exo/src/exo-makers.js").Guarded<{ + receive(payment: Payment): Promise>; + }>; + getOffersFacet(): import("@endo/exo/src/exo-makers.js").Guarded<{ + executeOffer(offerSpec: import("./smartWallet.js").OfferSpec): Promise; + tryExitOffer(offerId: import("./smartWallet.js").OfferId): Promise; + }>; + getCurrentSubscriber(): Subscriber; + getUpdatesSubscriber(): Subscriber; + getPublicTopics(): { + current: { + description: string; + subscriber: Subscriber; + storagePath: Promise; + }; + updates: { + description: string; + subscriber: Subscriber; + storagePath: Promise; + }; + }; + repairWalletForIncarnation2(key: any): void; + } & import("@endo/exo/src/get-interface.js").GetInterfaceGuard<{ + handleBridgeAction(actionCapData: import("@endo/marshal").CapData, canSpend?: boolean | undefined): Promise; + getDepositFacet(): import("@endo/exo/src/exo-makers.js").Guarded<{ + receive(payment: Payment): Promise>; + }>; + getOffersFacet(): import("@endo/exo/src/exo-makers.js").Guarded<{ + executeOffer(offerSpec: import("./smartWallet.js").OfferSpec): Promise; + tryExitOffer(offerId: import("./smartWallet.js").OfferId): Promise; + }>; + getCurrentSubscriber(): Subscriber; + getUpdatesSubscriber(): Subscriber; + getPublicTopics(): { + current: { + description: string; + subscriber: Subscriber; + storagePath: Promise; + }; + updates: { + description: string; + subscriber: Subscriber; + storagePath: Promise; + }; + }; + repairWalletForIncarnation2(key: any): void; + }> & import("@endo/eventual-send").RemotableBrand<{}, { + handleBridgeAction(actionCapData: import("@endo/marshal").CapData, canSpend?: boolean | undefined): Promise; + getDepositFacet(): import("@endo/exo/src/exo-makers.js").Guarded<{ + receive(payment: Payment): Promise>; + }>; + getOffersFacet(): import("@endo/exo/src/exo-makers.js").Guarded<{ + executeOffer(offerSpec: import("./smartWallet.js").OfferSpec): Promise; + tryExitOffer(offerId: import("./smartWallet.js").OfferId): Promise; + }>; + getCurrentSubscriber(): Subscriber; + getUpdatesSubscriber(): Subscriber; + getPublicTopics(): { + current: { + description: string; + subscriber: Subscriber; + storagePath: Promise; + }; + updates: { + description: string; + subscriber: Subscriber; + storagePath: Promise; + }; + }; + repairWalletForIncarnation2(key: any): void; + } & import("@endo/exo/src/get-interface.js").GetInterfaceGuard<{ + handleBridgeAction(actionCapData: import("@endo/marshal").CapData, canSpend?: boolean | undefined): Promise; + getDepositFacet(): import("@endo/exo/src/exo-makers.js").Guarded<{ + receive(payment: Payment): Promise>; + }>; + getOffersFacet(): import("@endo/exo/src/exo-makers.js").Guarded<{ + executeOffer(offerSpec: import("./smartWallet.js").OfferSpec): Promise; + tryExitOffer(offerId: import("./smartWallet.js").OfferId): Promise; + }>; + getCurrentSubscriber(): Subscriber; + getUpdatesSubscriber(): Subscriber; + getPublicTopics(): { + current: { + description: string; + subscriber: Subscriber; + storagePath: Promise; + }; + updates: { + description: string; + subscriber: Subscriber; + storagePath: Promise; + }; + }; + repairWalletForIncarnation2(key: any): void; + }>>, isNew: boolean]>; + }>; +}>; +export type SmartWalletContractTerms = { + agoricNames: ERef; + board: ERef; + assetPublisher: AssetPublisher; +}; +export type NameHub = import('@agoric/vats').NameHub; +export type AssetPublisher = { + getAssetSubscription: () => ERef>; +}; +export type isRevive = boolean; +export type WalletReviver = { + reviveWallet: (address: string) => Promise; + ackWallet: (address: string) => isRevive; +}; +export type start = typeof prepare; +//# sourceMappingURL=walletFactory.d.ts.map \ No newline at end of file From 9ecc0896fd5fe37732a941041a804ed849c105ee Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Sat, 2 Mar 2024 19:50:19 -0700 Subject: [PATCH 2/6] chore: vendor ERTP types --- .../src/@types/ertp/types-ambient.d.ts | 426 ++++++++++++++++++ packages/ag-trade/src/typeGuards.js | 6 + 2 files changed, 432 insertions(+) create mode 100644 packages/ag-trade/src/@types/ertp/types-ambient.d.ts create mode 100644 packages/ag-trade/src/typeGuards.js diff --git a/packages/ag-trade/src/@types/ertp/types-ambient.d.ts b/packages/ag-trade/src/@types/ertp/types-ambient.d.ts new file mode 100644 index 0000000..b10e5c0 --- /dev/null +++ b/packages/ag-trade/src/@types/ertp/types-ambient.d.ts @@ -0,0 +1,426 @@ +export type { + IssuerKit, + Mint, + Issuer, + Brand, + DisplayInfo, + Payment, + Purse, + Amount, + AssetKind, + NatValue, +}; + +/** + * Amounts are descriptions of digital assets, + * answering the questions "how much" and "of what kind". Amounts are values + * labeled with a brand. AmountMath executes the logic of how amounts are + * changed when digital assets are merged, separated, or otherwise + * manipulated. For example, a deposit of 2 bucks into a purse that already + * has 3 bucks gives a new purse balance of 5 bucks. An empty purse has 0 + * bucks. AmountMath relies heavily on polymorphic MathHelpers, which + * manipulate the unbranded portion. + */ +type Amount = { + brand: Brand; + value: AssetValueForKind; +}; +/** + * An `AmountValue` describes a set or quantity of assets that can be owned or + * shared. + * + * A fungible `AmountValue` uses a non-negative bigint to represent a quantity + * of that many assets. + * + * A non-fungible `AmountValue` uses an array or CopySet of `Key`s to represent + * a set of whatever asset each key represents. A `Key` is a passable value + * that can be used as an element in a set (SetStore or CopySet) or as the key + * in a map (MapStore or CopyMap). + * + * `SetValue` is for the deprecated set representation, using an array directly + * to represent the array of its elements. `CopySet` is the proper + * representation using a CopySet. + * + * A semi-fungible `CopyBag` is represented as a `CopyBag` of `Key` objects. + * "Bag" is synonymous with MultiSet, where an element of a bag can be present + * once or more times, i.e., some positive bigint number of times, + * representing that quantity of the asset represented by that key. + */ +type AmountValue = + | NatValue + | any[] + | CopySet + | import('@endo/patterns').CopyBag; +/** + * See doc-comment + * for `AmountValue`. + */ +type AssetKind = 'nat' | 'set' | 'copySet' | 'copyBag'; +type AssetValueForKind = K extends 'nat' + ? NatValue + : K extends 'set' + ? any[] + : K extends 'copySet' + ? CopySet + : K extends 'copyBag' + ? import('@endo/patterns').CopyBag + : never; +type AssetKindForValue = V extends NatValue + ? 'nat' + : V extends any[] + ? 'set' + : V extends CopySet + ? 'copySet' + : V extends import('@endo/patterns').CopyBag + ? 'copyBag' + : never; +type DisplayInfo = { + /** + * Tells the display software how many + * decimal places to move the decimal over to the left, or in other words, + * which position corresponds to whole numbers. We require fungible digital + * assets to be represented in integers, in the smallest unit (i.e. USD might + * be represented in mill, a thousandth of a dollar. In that case, + * `decimalPlaces` would be 3.) This property is optional, and for + * non-fungible digital assets, should not be specified. The decimalPlaces + * property should be used for _display purposes only_. Any other use is an + * anti-pattern. + */ + decimalPlaces?: number | undefined; + /** + * - the kind of asset, either AssetKind.NAT (fungible) + * or AssetKind.SET or AssetKind.COPY_SET (non-fungible) + */ + assetKind: K; +}; +/** + * The brand identifies the kind of issuer, and has a + * function to get the alleged name for the kind of asset described. The + * alleged name (such as 'BTC' or 'moola') is provided by the maker of the + * issuer and should not be trusted as accurate. + * + * Every amount created by a particular AmountMath will share the same brand, + * but recipients cannot rely on the brand to verify that a purported amount + * represents the issuer they intended, since the same brand can be reused by + * a misbehaving issuer. + */ +type Brand = { + /** + * Should be used with `issuer.getBrand` to ensure an issuer and brand match. + */ + isMyIssuer: (allegedIssuer: ERef) => Promise; + getAllegedName: () => string; + /** + * Give information to UI on how + * to display the amount. + */ + getDisplayInfo: () => DisplayInfo; + getAmountShape: () => Pattern; +}; +/** + * Return true if the payment continues to exist. + * + * If the payment is a promise, the operation will proceed upon fulfillment. + */ +type IssuerIsLive = (payment: ERef) => Promise; +/** + * Get the amount of digital assets in the payment. + * Because the payment is not trusted, we cannot call a method on it directly, + * and must use the issuer instead. + * + * If the payment is a promise, the operation will proceed upon fulfillment. + */ +type IssuerGetAmountOf = ( + payment: ERef, +) => Promise>; +/** + * Burn all of the digital assets in the payment. + * `optAmountShape` is optional. If the `optAmountShape` pattern is present, + * the amount of the digital assets in the payment must match + * `optAmountShape`, to prevent sending the wrong payment and other + * confusion. + * + * If the payment is a promise, the operation will proceed upon fulfillment. + * + * As always with optional `Pattern` arguments, keep in mind that technically + * the value `undefined` itself is a valid `Key` and therefore a valid + * `Pattern`. But in optional pattern position, a top level `undefined` will + * be interpreted as absence. If you want to express a `Pattern` that will + * match only `undefined`, use `M.undefined()` instead. + */ +type IssuerBurn = ( + payment: ERef, + optAmountShape?: Pattern, +) => Promise; +/** + * The issuer cannot mint a new amount, but it can + * create empty purses and payments. The issuer can also transform payments + * (splitting payments, combining payments, burning payments, and claiming + * payments exclusively). The issuer should be gotten from a trusted source + * and then relied upon as the decider of whether an untrusted payment is + * valid. + */ +type Issuer = { + /** + * Get the Brand for this Issuer. The Brand + * indicates the type of digital asset and is shared by the mint, the issuer, + * and any purses and payments of this particular kind. The brand is not + * closely held, so this function should not be trusted to identify an issuer + * alone. Fake digital assets and amount can use another issuer's brand. + */ + getBrand: () => Brand; + /** + * Get the allegedName for this + * mint/issuer + */ + getAllegedName: () => string; + /** + * Get the kind of MathHelpers used by + * this Issuer. + */ + getAssetKind: () => AssetKind; + /** + * Give information to UI on how + * to display amounts for this issuer. + */ + getDisplayInfo: () => DisplayInfo; + /** + * Make an empty purse of this brand. + */ + makeEmptyPurse: () => Purse; + isLive: IssuerIsLive; + getAmountOf: IssuerGetAmountOf; + burn: IssuerBurn; +}; +type PaymentLedger = { + mint: Mint; + /** + * Externally useful only if this issuer + * uses recovery sets. Can be used to get the recovery set associated with + * minted payments that are still live. + */ + mintRecoveryPurse: Purse; + issuer: Issuer; + brand: Brand; +}; +type IssuerKit = { + mint: Mint; + /** + * Externally useful only if this issuer + * uses recovery sets. Can be used to get the recovery set associated with + * minted payments that are still live. + */ + mintRecoveryPurse: Purse; + issuer: Issuer; + brand: Brand; + displayInfo: DisplayInfo; +}; +type AdditionalDisplayInfo = { + /** + * Tells the display software how many + * decimal places to move the decimal over to the left, or in other words, + * which position corresponds to whole numbers. We require fungible digital + * assets to be represented in integers, in the smallest unit (i.e. USD might + * be represented in mill, a thousandth of a dollar. In that case, + * `decimalPlaces` would be 3.) This property is optional, and for + * non-fungible digital assets, should not be specified. The decimalPlaces + * property should be used for _display purposes only_. Any other use is an + * anti-pattern. + */ + decimalPlaces?: number | undefined; + assetKind?: AssetKind | undefined; +}; +type ShutdownWithFailure = import('@agoric/swingset-vat').ShutdownWithFailure; +/** + * Holding a Mint carries the right to issue new digital + * assets. These assets all have the same kind, which is called a Brand. + */ +type Mint = { + /** + * Gets the Issuer for this mint. + */ + getIssuer: () => Issuer; + /** + * Creates a new + * Payment containing newly minted amount. + */ + mintPayment: (newAmount: Amount) => Payment; +}; +/** + * Issuers first became durable with mandatory recovery sets. Later they were + * made optional, but there is no support for converting from one state to the + * other. Thus, absence of a `RecoverySetsOption` state is equivalent to + * `'hasRecoverySets'`. In the absence of a `recoverySetsOption` parameter, + * upgradeIssuerKit defaults to the predecessor's `RecoverySetsOption` state, or + * `'hasRecoverySets'` if none. + * + * At this time, issuers started in one of the states (`'noRecoverySets'`, or + * `'hasRecoverySets'`) cannot be converted to the other on upgrade. If this + * transition is needed, it can likely be supported in a future upgrade. File an + * issue on github and explain what you need and why. + */ +type RecoverySetsOption = 'hasRecoverySets' | 'noRecoverySets'; +type DepositFacetReceive = ( + payment: Payment, + optAmountShape?: Pattern, +) => Amount; +type DepositFacet = { + /** + * Deposit all the contents of payment + * into the purse that made this facet, returning the amount. If the optional + * argument `optAmount` does not equal the amount of digital assets in the + * payment, throw an error. + * + * If payment is a promise, throw an error. + */ + receive: DepositFacetReceive; +}; +type PurseDeposit = ( + payment: Payment, + optAmountShape?: Pattern, +) => Amount; +/** + * Purses hold amount of digital assets of the same + * brand, but unlike Payments, they are not meant to be sent to others. To + * transfer digital assets, a Payment should be withdrawn from a Purse. The + * amount of digital assets in a purse can change through the action of + * deposit() and withdraw(). + * + * The primary use for Purses and Payments is for currency-like and goods-like + * digital assets, but they can also be used to represent other kinds of + * rights, such as the right to participate in a particular contract. + */ +type Purse = { + /** + * Get the alleged Brand for this + * Purse + */ + getAllegedBrand: () => Brand; + /** + * Get the amount contained in this + * purse. + */ + getCurrentAmount: () => Amount; + /** + * Get a lossy + * notifier for changes to this purse's balance. + */ + getCurrentAmountNotifier: () => LatestTopic>; + /** + * Deposit all the contents of payment into + * this purse, returning the amount. If the optional argument `optAmount` does + * not equal the amount of digital assets in the payment, throw an error. + * + * If payment is a promise, throw an error. + */ + deposit: PurseDeposit; + /** + * Return an object whose + * `receive` method deposits to the current Purse. + */ + getDepositFacet: () => DepositFacet; + /** + * Withdraw amount from + * this purse into a new Payment. + */ + withdraw: (amount: Amount) => Payment; + /** + * The set of payments + * withdrawn from this purse that are still live. These are the payments that + * can still be recovered in emergencies by, for example, depositing into this + * purse. Such a deposit action is like canceling an outstanding check because + * you're tired of waiting for it. Once your cancellation is acknowledged, you + * can spend the assets at stake on other things. Afterwards, if the recipient + * of the original check finally gets around to depositing it, their deposit + * fails. + * + * Returns an empty set if this issuer does not support recovery sets. + */ + getRecoverySet: () => CopySet>; + /** + * For use in emergencies, such as coming + * back from a traumatic crash and upgrade. This deposits all the payments in + * this purse's recovery set into the purse itself, returning the total amount + * of assets recovered. + * + * Returns an empty amount if this issuer does not support recovery sets. + */ + recoverAll: () => Amount; +}; +/** + * Payments hold amount of digital assets of the same + * brand in transit. Payments can be deposited in purses, split into multiple + * payments, combined, and claimed (getting an exclusive payment). Payments + * are linear, meaning that either a payment has the same amount of digital + * assets it started with, or it is used up entirely. It is impossible to + * partially use a payment. + * + * Payments are often received from other actors and therefore should not be + * trusted themselves. To get the amount of digital assets in a payment, use + * the trusted issuer: issuer.getAmountOf(payment), + * + * Payments can be converted to Purses by getting a trusted issuer and calling + * `issuer.makeEmptyPurse()` to create a purse, then + * `purse.deposit(payment)`. + */ +type Payment = { + /** + * Get the allegedBrand, indicating + * the type of digital asset this payment purports to be, and which issuer to + * use. Because payments are not trusted, any method calls on payments should + * be treated with suspicion and verified elsewhere. + */ + getAllegedBrand: () => Brand; +}; +/** + * All of the difference in how digital asset + * amount are manipulated can be reduced to the behavior of the math on + * values. We extract this custom logic into mathHelpers. MathHelpers are + * about value arithmetic, whereas AmountMath is about amounts, which are the + * values labeled with a brand. AmountMath use mathHelpers to do their value + * arithmetic, and then brand the results, making a new amount. + * + * The MathHelpers are designed to be called only from AmountMath, and so all + * methods but coerce can assume their inputs are valid. They only need to do + * output validation, and only when there is a possibility of invalid output. + */ +type MathHelpers = { + /** + * Check the kind of this value and + * throw if it is not the expected kind. + */ + doCoerce: (allegedValue: V) => V; + /** + * Get the representation for the identity + * element (often 0 or an empty array) + */ + doMakeEmpty: () => V; + /** + * Is the value the identity + * element? + */ + doIsEmpty: (value: V) => boolean; + /** + * Is the left greater than + * or equal to the right? + */ + doIsGTE: (left: V, right: V) => boolean; + /** + * Does left equal right? + */ + doIsEqual: (left: V, right: V) => boolean; + /** + * Return the left combined with the + * right. + */ + doAdd: (left: V, right: V) => V; + /** + * Return what remains after + * removing the right from the left. If something in the right was not in the + * left, we throw an error. + */ + doSubtract: (left: V, right: V) => V; +}; +type NatValue = bigint; +type SetValue = Key[]; +//# sourceMappingURL=types-ambient.d.ts.map diff --git a/packages/ag-trade/src/typeGuards.js b/packages/ag-trade/src/typeGuards.js new file mode 100644 index 0000000..c7f4970 --- /dev/null +++ b/packages/ag-trade/src/typeGuards.js @@ -0,0 +1,6 @@ +// @ts-check +import { M } from '@endo/patterns'; + +// XXX copied from @agoric/ertp to avoid importing / building xsnap +export const BrandShape = M.remotable('Brand'); +export const AmountShape = harden({ brand: BrandShape, value: M.any() }); From 220db481ff9a02c7e259c78be6d08d35cf5b7257 Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Sat, 2 Mar 2024 19:51:20 -0700 Subject: [PATCH 3/6] chore: parseRatio, types --- packages/ag-trade/src/parseRatio.js | 58 +++++++++++++++++++++++++++++ packages/ag-trade/src/types.d.ts | 6 +++ packages/ag-trade/src/types.js | 1 + 3 files changed, 65 insertions(+) create mode 100644 packages/ag-trade/src/parseRatio.js create mode 100644 packages/ag-trade/src/types.d.ts create mode 100644 packages/ag-trade/src/types.js diff --git a/packages/ag-trade/src/parseRatio.js b/packages/ag-trade/src/parseRatio.js new file mode 100644 index 0000000..b982060 --- /dev/null +++ b/packages/ag-trade/src/parseRatio.js @@ -0,0 +1,58 @@ +// @ts-check + +const { Fail } = assert; + +const NUMERIC_RE = /^(\d\d*)(?:\.(\d*))?$/; +/** @typedef {bigint | number | string} ParsableNumber */ + +/** @typedef {import('./@types/ertp/types-ambient').AssetKind} AssetKind */ +/** @template {AssetKind} K @typedef {import('./@types/ertp/types-ambient').Brand} Brand */ +/** @typedef {import('./@types/ertp/types-ambient').NatValue} NatValue */ +/** @typedef {import('./types').Ratio} Ratio */ + +const PERCENT = 100n; + +/** + * @param {NatValue} numerator + * @param {Brand<'nat'>} numeratorBrand + * @param {NatValue} denominator + * @param {Brand<'nat'>} denominatorBrand + * @returns {Ratio} + */ +const makeRatio = ( + numerator, + numeratorBrand, + denominator = PERCENT, + denominatorBrand = numeratorBrand, +) => + harden({ + numerator: { brand: numeratorBrand, value: numerator }, + denominator: { brand: denominatorBrand, value: denominator }, + }); + +/** + * Create a ratio from a given numeric value. + * + * @param {ParsableNumber} numeric + * @param {Brand<'nat'>} numeratorBrand + * @param {Brand<'nat'>} [denominatorBrand] + * @returns {Ratio} + */ +export const parseRatio = ( + numeric, + numeratorBrand, + denominatorBrand = numeratorBrand, +) => { + const match = `${numeric}`.match(NUMERIC_RE); + if (!match) { + throw Fail`Invalid numeric data: ${numeric}`; + } + + const [_, whole, part = ''] = match; + return makeRatio( + BigInt(`${whole}${part}`), + numeratorBrand, + 10n ** BigInt(part.length), + denominatorBrand, + ); +}; diff --git a/packages/ag-trade/src/types.d.ts b/packages/ag-trade/src/types.d.ts new file mode 100644 index 0000000..f31d038 --- /dev/null +++ b/packages/ag-trade/src/types.d.ts @@ -0,0 +1,6 @@ +import type { OfferSpec } from './@types/smart-wallet/smartWallet.js'; +import type { Amount, Brand } from './@types/ertp/types-ambient.js'; + +export type { OfferSpec, Amount, Brand }; + +export type Ratio = { numerator: Amount<'nat'>; denominator: Amount<'nat'> }; diff --git a/packages/ag-trade/src/types.js b/packages/ag-trade/src/types.js new file mode 100644 index 0000000..9a6e54d --- /dev/null +++ b/packages/ag-trade/src/types.js @@ -0,0 +1 @@ +// see types.d.ts From 2a411c624b1b74e9c14df42c962b9bb774ad8763 Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Sat, 2 Mar 2024 19:51:44 -0700 Subject: [PATCH 4/6] buid(ag-trade): patterns dep --- packages/ag-trade/package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/ag-trade/package.json b/packages/ag-trade/package.json index a87d2f8..6d330bc 100644 --- a/packages/ag-trade/package.json +++ b/packages/ag-trade/package.json @@ -41,7 +41,8 @@ "@cosmjs/proto-signing": "^0.31.3", "@cosmjs/stargate": "^0.31.3", "@endo/far": "^0.2.21", - "@endo/marshal": "^1.0.1" + "@endo/marshal": "^1.0.1", + "@endo/patterns": "^1.2.0" }, "devDependencies": { "@endo/init": "^1.0.1", From 1d5537cfa5aafed30034cd8816d9e69aa06483eb Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Sat, 2 Mar 2024 19:52:26 -0700 Subject: [PATCH 5/6] build(ag-trade): lock packages with patterns --- yarn.lock | 80 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/yarn.lock b/yarn.lock index 7c10204..835ef78 100644 --- a/yarn.lock +++ b/yarn.lock @@ -170,6 +170,15 @@ resolved "https://registry.yarnpkg.com/@endo/base64/-/base64-1.0.0.tgz#f44f0378fc960ab4e986b0452935f27d93726ca1" integrity sha512-rd46CY2jk3oblrxpH7gC+xvnUIbCrN1geEb+IgKSx17WKcT0fPgrwFqnWnSzXCyJqIJ7xkeAUvEbilPQoZMxmg== +"@endo/common@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@endo/common/-/common-1.1.0.tgz#c4f174866b6179b5e82f8b479e6afd0a9de61e38" + integrity sha512-oDYx3Osr843NNZ5jvRUgcyYMu56MJHp+CPHjVDKXxziKuD7DE0RGa61FWY8XcMwMw3fQ8O5oJzdtGcqPXA89yA== + dependencies: + "@endo/errors" "^1.1.0" + "@endo/eventual-send" "^1.1.2" + "@endo/promise-kit" "^1.0.4" + "@endo/env-options@^0.1.4": version "0.1.4" resolved "https://registry.yarnpkg.com/@endo/env-options/-/env-options-0.1.4.tgz#e516bc3864f00b154944e444fb8996a9a0c23a45" @@ -180,6 +189,18 @@ resolved "https://registry.yarnpkg.com/@endo/env-options/-/env-options-1.0.1.tgz#a6ad1951f3303426cd15956aa7b95ea06cb34ad0" integrity sha512-5hieu6ow9Kgf2wKKchE1xQEN7VlKVLL3O0eEjxN9d52XodHMbEFu0gGoFA6NeQJq9SHNrbgJhZDfMkPaHvoFxg== +"@endo/env-options@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@endo/env-options/-/env-options-1.1.1.tgz#eee630f8eff01580ec49e0dedcb1b6cef05d89a4" + integrity sha512-uCwlJ8Vkndx/VBBo36BdYHdxSoQPy7ZZpwyJNfv86Rh4B1IZfqzCRPf0u0mPgJdzOr7lShQey60SuYwoMSZ9Xg== + +"@endo/errors@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@endo/errors/-/errors-1.1.0.tgz#b245be9007eb963c4176ba8cd8b99d2cfe15e344" + integrity sha512-Z+5gIlLaXSkq3uW6qlRf54HiuyHHA8/9o/ptI04y2z6e+lRinsmjmyXfG93DG0zPOrY6rCqgKCbKsHu8I5P3Yg== + dependencies: + ses "^1.3.0" + "@endo/eventual-send@^0.17.6": version "0.17.6" resolved "https://registry.yarnpkg.com/@endo/eventual-send/-/eventual-send-0.17.6.tgz#86719e4e3ff76991c49f6680309dc77dff65fe55" @@ -194,6 +215,13 @@ dependencies: "@endo/env-options" "^1.0.1" +"@endo/eventual-send@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@endo/eventual-send/-/eventual-send-1.1.2.tgz#496e97c572462d2552a114810ace61af548bdb1c" + integrity sha512-6zax3s7cJv4fHt3r9yQcgyllXYCfOtTqFUYkNMRMOxewwY80nz18H2MlhL9PJfOBHzTo34O3SDgpZvcMPxdmRA== + dependencies: + "@endo/env-options" "^1.1.1" + "@endo/far@^0.2.21": version "0.2.22" resolved "https://registry.yarnpkg.com/@endo/far/-/far-0.2.22.tgz#fda187289a903ee3f9d6dcc5664ee7fef1994b1f" @@ -229,11 +257,28 @@ "@endo/pass-style" "^1.0.1" "@endo/promise-kit" "^1.0.1" +"@endo/marshal@^1.3.0": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@endo/marshal/-/marshal-1.3.0.tgz#36a6de89c70bdd8ce0b81d0a02ca98264b3d1200" + integrity sha512-8xhllnDieGa8sH+/yRJSSs2pUTXTaR1x7FXQcMih1rwgdiA9n0KpdZyg7fjncCS82AEoEAqm3tGi/puIEoCdBg== + dependencies: + "@endo/common" "^1.1.0" + "@endo/errors" "^1.1.0" + "@endo/eventual-send" "^1.1.2" + "@endo/nat" "^5.0.4" + "@endo/pass-style" "^1.2.0" + "@endo/promise-kit" "^1.0.4" + "@endo/nat@^5.0.1": version "5.0.1" resolved "https://registry.yarnpkg.com/@endo/nat/-/nat-5.0.1.tgz#ab21329764a32edbc492a51eb29443866ac26a39" integrity sha512-L2ZY7om+mHS/a+DLCezanbIXUtLW//tnVPfbKa7m4UAgo+JsDRGF9WyyS8P77cYcP3aWVelbntiTt0rCd/grzQ== +"@endo/nat@^5.0.4": + version "5.0.4" + resolved "https://registry.yarnpkg.com/@endo/nat/-/nat-5.0.4.tgz#fe6f4adad5385a7fe359417a9ea783531f2b1c48" + integrity sha512-LuJpw5QcIcLE/l9CUG48S9KpxpqdKsbpWVmTJmpAazcRTuv+NUwsaisIrgyaVeiMRmB7T39rAW3a+6j5d6B0Jw== + "@endo/pass-style@^0.1.7": version "0.1.7" resolved "https://registry.yarnpkg.com/@endo/pass-style/-/pass-style-0.1.7.tgz#ea22568e8b86fb2d1a14a5fc042374cc0d8e310b" @@ -251,6 +296,27 @@ "@endo/promise-kit" "^1.0.1" "@fast-check/ava" "^1.1.5" +"@endo/pass-style@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@endo/pass-style/-/pass-style-1.2.0.tgz#dc7bc4f616baf6a7ed196e0c0a3293666ec51f68" + integrity sha512-ISJX9ocbU03ZQ6+1WmUi6qMrvtOCbKpPErsxh4lqdzKhh1MZISzOmGTtoEa0zc1VGMJFVZ3gCvC173bVSft2pg== + dependencies: + "@endo/errors" "^1.1.0" + "@endo/eventual-send" "^1.1.2" + "@endo/promise-kit" "^1.0.4" + "@fast-check/ava" "^1.1.5" + +"@endo/patterns@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@endo/patterns/-/patterns-1.2.0.tgz#5932feb051e2b966ed010aa14f0428b1712d91a2" + integrity sha512-uZaN7nMSW7wWB1Nhyxpppowmchc//Z+UvilakQqrN8uUzX0qUKmTgviS4vKtCmsFtLXlfI13uqp0k8OPr3ClpQ== + dependencies: + "@endo/common" "^1.1.0" + "@endo/errors" "^1.1.0" + "@endo/eventual-send" "^1.1.2" + "@endo/marshal" "^1.3.0" + "@endo/promise-kit" "^1.0.4" + "@endo/promise-kit@^0.2.60": version "0.2.60" resolved "https://registry.yarnpkg.com/@endo/promise-kit/-/promise-kit-0.2.60.tgz#8012ada06970c7eaf965cd856563b34a1790e163" @@ -265,6 +331,13 @@ dependencies: ses "^1.0.1" +"@endo/promise-kit@^1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@endo/promise-kit/-/promise-kit-1.0.4.tgz#809569fe23af9a065a311aa11747e5f00c6e481c" + integrity sha512-b4U6S7wZpfXw9BVSvbRHo0VvdznHsJPrHiMSg+2Upw7W1uVrT1SzDTu4bi4gL0udptCD6wGYJ7bIrMjShyzvww== + dependencies: + ses "^1.3.0" + "@endo/ses-ava@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@endo/ses-ava/-/ses-ava-1.0.1.tgz#b29ce458ab5e84894a8febc604e3473a86c5fc13" @@ -6850,6 +6923,13 @@ ses@^1.0.1: dependencies: "@endo/env-options" "^1.0.1" +ses@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/ses/-/ses-1.3.0.tgz#4de8a2e740e5ff9e3cdbc4fd4a3574075c493f40" + integrity sha512-TURVgXm/fs38N4iJfhU9NjUiNvnU7Z/G7gVjM17jD+nrChRzMmR57fbvAzbQeGCS8Cm0m1fBs0jYCqmU6GZ7Tg== + dependencies: + "@endo/env-options" "^1.1.1" + set-blocking@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" From 65044d921fbda71c7c657a7ce2a0bbe44c9c3314 Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Sat, 2 Mar 2024 19:53:02 -0700 Subject: [PATCH 6/6] feat(ag-trade): list bids; place bid by discount --- packages/ag-trade/src/auction-bid.js | 189 +++++++++++++++++++++++++++ 1 file changed, 189 insertions(+) create mode 100644 packages/ag-trade/src/auction-bid.js diff --git a/packages/ag-trade/src/auction-bid.js b/packages/ag-trade/src/auction-bid.js new file mode 100644 index 0000000..715f213 --- /dev/null +++ b/packages/ag-trade/src/auction-bid.js @@ -0,0 +1,189 @@ +// @ts-check +import { E } from '@endo/far'; +import { M, mustMatch, matches } from '@endo/patterns'; + +import { AmountShape } from './typeGuards.js'; +import { parseRatio } from './parseRatio.js'; + +/** @template T @typedef {import('@endo/eventual-send').ERef} ERef */ + +const { Fail, quote: q } = assert; +const UNIT6 = 1_000_000n; + +/** + * @template T + * @param {T | null | undefined } x + * @returns {T} + */ +const NonNullish = x => { + if (x === undefined || x === null) throw assert.error('NonNullish'); + return x; +}; + +const bidInvitationShape = harden({ + source: 'agoricContract', + instancePath: ['auctioneer'], + callPipe: [['makeBidInvitation', M.any()]], +}); + +/** + * @param {{ + * offerId: string; + * give: import('./types.js').Amount<'nat'>; + * maxBuy: import('./types.js').Amount<'nat'>; + * wantMinimum?: import('./types.js').Amount<'nat'>; + * } & ( + * | { + * price: number; + * } + * | { + * discount: number; // -1 to 1. e.g. 0.10 for 10% discount, -0.05 for 5% markup + * } + * )} opts + * @returns {import('./types.js').OfferSpec} + */ +const makeBidOffer = opts => { + mustMatch( + harden({ + offerId: opts.offerId, + give: opts.give, + maxBuy: opts.maxBuy, + }), + harden({ + offerId: M.or(M.string(), M.number()), + give: AmountShape, + maxBuy: AmountShape, + }), + ); + const proposal = { + give: { Bid: opts.give }, + ...(opts.wantMinimum ? { want: { Collateral: opts.wantMinimum } } : {}), + }; + const istBrand = proposal.give.Bid.brand; + const maxBuy = opts.maxBuy; + + const bounds = (x, lo, hi) => { + assert(x >= lo && x <= hi); + return x; + }; + + assert( + 'price' in opts || 'discount' in opts, + 'must specify price or discount', + ); + const offerArgs = + 'price' in opts + ? { + maxBuy: opts.maxBuy, + offerPrice: parseRatio(opts.price, istBrand, maxBuy.brand), + } + : { + maxBuy, + offerBidScaling: parseRatio( + (1 - bounds(opts.discount, -1, 1)).toFixed(2), + istBrand, + istBrand, + ), + }; + + /** @type {import('./types.js').OfferSpec} */ + const offerSpec = { + id: opts.offerId, + invitationSpec: { + source: 'agoricContract', + instancePath: ['auctioneer'], + callPipe: [['makeBidInvitation', [maxBuy.brand]]], + }, + proposal, + offerArgs, + }; + return offerSpec; +}; + +/** + * @param {string[]} args + * @param {{ [k: string]: boolean | undefined }} [style] + */ +const getopts = (args, style = {}) => { + /** @type {{ [k: string]: string}} */ + const flags = {}; + while (args.length > 0) { + const arg = NonNullish(args.shift()); + if (arg.startsWith('--')) { + const name = arg.slice('--'.length); + if (style[name] === true) { + flags[name] = ''; + continue; + } + if (args.length <= 0) throw RangeError(`no value for ${arg}`); + flags[name] = NonNullish(args.shift()); + } + } + return harden(flags); +}; + +/** + * @param {*} self + * @param {string[]} args + */ +export const main = async (self, ...args) => { + const flags = getopts(args, { list: true }); + /** @type {import("./smartWallet.js").WalletKit} */ + const { query: vstorage, smartWallet } = await E(self).lookup('wallet'); + + if ('list' in flags) { + const info = await E(E(smartWallet).readOnly()).current(); + for (const [_id, offerStatus] of info.liveOffers) { + if (!matches(offerStatus.invitationSpec, bidInvitationShape)) continue; + console.log(q(offerStatus).toString()); + } + return; + } else { + mustMatch( + flags, + M.splitRecord( + { give: M.string(), discount: M.string(), maxBuy: M.string() }, + { wantMinimum: M.string() }, + ), + ); + } + + const vse = await E( + E(vstorage).lookup('agoricNames', 'vbankAsset'), + ).entries(); + const byName = Object.fromEntries( + vse.map(([_denom, info]) => [info.issuerName, info]), + ); + console.log('vbankAssets', Object.keys(byName)); + + /** @param {string} flag */ + const parseAmount = flag => { + if (!(flag in flags)) throw Error(`missing ${flag}`); + const arg = flags[flag]; + const [numeral, brandName] = arg.split(' '); + brandName in byName || Fail`unknown brand: ${brandName}`; + const { brand, displayInfo } = byName[brandName]; + const { decimalPlaces } = displayInfo; + const value = BigInt(Number(numeral) * 10 ** decimalPlaces); + /** @type {import('./types').Amount<'nat'>} */ + const amt = { brand, value }; + return amt; + }; + + /** @type {ERef} */ + const fresh = E(self).lookup('fresh'); + const id = await E(fresh).next(); + + const offerSpec = makeBidOffer({ + offerId: id, + give: parseAmount('give'), + maxBuy: parseAmount('maxBuy'), + ...('wantMinimum' in flags && { + wantMinimum: parseAmount('wantMinimum'), + }), + discount: Number(flags['discount']), + }); + + const info = await E(smartWallet).executeOffer(offerSpec); + return info; +};