Skip to content
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(smart-wallet): trading in well-known non-vbank assets #8071

Merged
merged 6 commits into from
Aug 3, 2023

Conversation

dckc
Copy link
Member

@dckc dckc commented Jul 20, 2023

refs: #7226

Description

Enrich the offer and deposit handling in the smart wallet to support well-known issuers -- that is, issuers in agoricNames.issuer and correspondingly agoricNames.brand.

Also, demonstrate in a test how permissioned contract deployment would enable trading in non-vbank assets.

Security Considerations

To date, aside from EC member invitations and Oracle operator invitations, the smart wallet contract held no precious assets; assets such as IST and USDC are reflected from cosmos bank module balances via the vbank.

This PR adds purses that hold assets that are not recorded anywhere else.

Scaling Considerations

When we make a new purse from a well-known issuer, we do a reverse-lookup by brand. One approach would be to add a method on NameHub for that, as discussed in...

The current approach here is to use the existing E(E(agoricNames).lookup('brand')).entries() API and then search thru the entries on the caller's side.

Documentation Considerations

The convention for publishing { displayInfo } at published.boardAux.board0123 is only documented in a test. (That'll change in the subsequent upgrade PR).

agoricNamesAdmin is noted in the discussion of consume under Permisioned Deployment, but the whole NameHub API is more or less undocumented:

Testing Considerations

The tests tell a bit of a story using t.log(). (details below)

✔ trading in non-vbank asset: game real-estate NFTs (2.7s)
✔ non-vbank asset: give before deposit

Given that we're introducing purses that store precious assets, would it be cost-effective to test that we know how to trace purse ownership and balance at the kernel database level? #8138

Upgrade Considerations

Upgrading the walletFactory contract and to publish displayInfo for IST etc. under boardAux is to follow in a separate PR.

@dckc dckc force-pushed the 7226-sw-nft-issuer branch from e881d04 to fc1d526 Compare July 20, 2023 07:53
@dckc dckc force-pushed the 7226-sw-nft-issuer branch 2 times, most recently from c569d5c to 41c25de Compare July 20, 2023 08:07
@dckc
Copy link
Member Author

dckc commented Jul 21, 2023

TDD: using a real contract shows we can't deposit NFT payouts

ref: test('trading in non-vbank assets', ...)

test run log
agoric-sdk/packages/smart-wallet$ yarn test test/test-addAsset.js -m '*non-vbank*'
yarn run v1.22.19
$ ava test/test-addAsset.js -m '*non-vbank*'

[bundleTool] bundles/ bundle-walletFactory.js valid: 148 files bundled at 2023-07-21T03:07:01.680Z 
----- WltFct.4  2 makeAssetRegistry Object [Alleged: Bank] {}
@@marshal for vstorage write [ [ 'IST', Object [Alleged: ZDEFAULT brand] {} ] ]
----- WltFct.4  3 registering asset IST
@@marshal for vstorage write [ [ 'game1', Object [Alleged: InstanceHandle] {} ] ]
@@marshal for vstorage write [
  [ 'IST', Object [Alleged: ZDEFAULT brand] {} ],
  [ 'Place', Object [Alleged: Place brand] {} ]
]
walletFactory.fromBridge: {
  blockHeight: 0,
  blockTime: 0,
  owner: 'agoric1player1',
  spendAction: '{"body":"#{\\"method\\":\\"executeOffer\\",\\"offer\\":{\\"id\\":\\"joinGame1234\\",\\"invitationSpec\\":{\\"instance\\":\\"$0.Alleged: SEVERED: InstanceHandle\\",\\"publicInvitationMaker\\":\\"makeJoinInvitation\\",\\"source\\":\\"contract\\"},\\"proposal\\":{\\"give\\":{\\"Price\\":{\\"brand\\":\\"$1.Alleged: SEVERED: ZDEFAULT brand\\",\\"value\\":\\"+250000\\"}},\\"want\\":{\\"Places\\":{\\"brand\\":\\"$2.Alleged: SEVERED: Place brand\\",\\"value\\":{\\"#tag\\":\\"copyBag\\",\\"payload\\":[[\\"Park Place\\",\\"+1\\"],[\\"Boardwalk\\",\\"+1\\"]]}}}}}}","slots":["board0592","board0371","board0223"]}',
  type: 'WALLET_SPEND_ACTION'
}
walletFactory: {
  wallet: Object [Alleged: SmartWallet self] {},
  actionCapData: {
    body: '#{"method":"executeOffer","offer":{"id":"joinGame1234","invitationSpec":{"instance":"$0.Alleged: SEVERED: InstanceHandle","publicInvitationMaker":"makeJoinInvitation","source":"contract"},"proposal":{"give":{"Price":{"brand":"$1.Alleged: SEVERED: ZDEFAULT brand","value":"+250000"}},"want":{"Places":{"brand":"$2.Alleged: SEVERED: Place brand","value":{"#tag":"copyBag","payload":[["Park Place","+1"],["Boardwalk","+1"]]}}}}}}',
    slots: [ 'board0592', 'board0371', 'board0223' ]
  }
}
wallet agoric1player1 starting executeOffer joinGame1234
join give {
  Price: { brand: Object [Alleged: ZDEFAULT brand] {}, value: 250000n }
} want Object [copyBag] {
  payload: [ [ 'Park Place', 1n ], [ 'Boardwalk', 1n ] ]
}
wallet agoric1player1 joinGame1234 seated
wallet agoric1player1 joinGame1234 offerResult string welcome to the game
wallet agoric1player1 offerStatus {
  id: 'joinGame1234',
  invitationSpec: {
    instance: Object [Alleged: InstanceHandle] {},
    publicInvitationMaker: 'makeJoinInvitation',
    source: 'contract'
  },
  proposal: { give: { Price: [Object] }, want: { Places: [Object] } },
  result: 'welcome to the game'
}
wallet agoric1player1 joinGame1234 numSatisfied 1
wallet agoric1player1 offerStatus {
  id: 'joinGame1234',
  invitationSpec: {
    instance: Object [Alleged: InstanceHandle] {},
    publicInvitationMaker: 'makeJoinInvitation',
    source: 'contract'
  },
  proposal: { give: { Price: [Object] }, want: { Places: [Object] } },
  result: 'welcome to the game',
  numWantsSatisfied: 1
}
wallet agoric1player1 offerStatus {
  id: 'joinGame1234',
  invitationSpec: {
    instance: Object [Alleged: InstanceHandle] {},
    publicInvitationMaker: 'makeJoinInvitation',
    source: 'contract'
  },
  proposal: { give: { Price: [Object] }, want: { Places: [Object] } },
  result: 'welcome to the game',
  numWantsSatisfied: 1,
  payouts: {
    Places: { brand: Object [Alleged: Place brand] {}, value: 0n },
    Price: { brand: Object [Alleged: ZDEFAULT brand] {}, value: 0n }
  }
}
  ✘ [fail]: trading in non-vbank assets
    ℹ @@mutating agoricNames and vstorage may interfere with other tests
    ℹ @@TODO: wallet UI needs displayInfo. where to get it??
    ℹ agoric1player1 approves offer: joinGame1234
  ─

  trading in non-vbank assets
  Difference (- actual, + expected):

    {
      status: {
        id: 'joinGame1234',
        invitationSpec: {
          publicInvitationMaker: 'makeJoinInvitation',
        },
        numWantsSatisfied: 1,
        payouts: {
          Places: {
  -         value: 0n,
  +         value: Object @copyBag {
  +           payload: [
  +             [
  +               'Park Place',
  +               1n,
  +             ],
  +             [
  +               'Boardwalk',
  +               1n,
  +             ],
  +           ],
  +         },
          },
        },
        result: 'welcome to the game',
      },
      updated: 'offerStatus',
    }

  › packages/smart-wallet/test/test-addAsset.js:330:5

  ─

  1 test failed
error Command failed with exit code 1.

@dckc
Copy link
Member Author

dckc commented Jul 25, 2023

smart wallet feature: stumped by "no ordinal" in test

maybe due to using less than a full swingset?

Is there some way to use packages/swingset-liveslots/tools/fakeVirtualSupport.js to address this?

3ec3eb1

p.s. diagnosis: was using .set() where I should have used .init()

stack trace etc. from test-addAsset.js
agoric-sdk/packages/smart-wallet$ yarn test test/test-addAsset.js -m '*non-vbank*'
yarn run v1.22.19
$ ava test/test-addAsset.js -m '*non-vbank*'

[bundleTool] bundles/ bundle-walletFactory.js valid: 148 files bundled at 2023-07-25T18:02:48.344Z 
----- WltFct.4  2 makeAssetRegistry Object [Alleged: Bank] {}
@@marshal for vstorage write [ [ 'IST', Object [Alleged: ZDEFAULT brand] {} ] ]
----- WltFct.4  3 registering asset IST
@@marshal for vstorage write [ [ 'game1', Object [Alleged: InstanceHandle] {} ] ]
@@marshal for vstorage write [
  [ 'IST', Object [Alleged: ZDEFAULT brand] {} ],
  [ 'Place', Object [Alleged: Place brand] {} ]
]
@@@how to add new property to exo state?
walletFactory.fromBridge: {
  blockHeight: 0,
  blockTime: 0,
  owner: 'agoric1player1',
  spendAction: '{"body":"#{\\"method\\":\\"executeOffer\\",\\"offer\\":{\\"id\\":\\"joinGame1234\\",\\"invitationSpec\\":{\\"instance\\":\\"$0.Alleged: SEVERED: InstanceHandle\\",\\"publicInvitationMaker\\":\\"makeJoinInvitation\\",\\"source\\":\\"contract\\"},\\"proposal\\":{\\"give\\":{\\"Price\\":{\\"brand\\":\\"$1.Alleged: SEVERED: ZDEFAULT brand\\",\\"value\\":\\"+250000\\"}},\\"want\\":{\\"Places\\":{\\"brand\\":\\"$2.Alleged: SEVERED: Place brand\\",\\"value\\":{\\"#tag\\":\\"copyBag\\",\\"payload\\":[[\\"Park Place\\",\\"+1\\"],[\\"Boardwalk\\",\\"+1\\"]]}}}}}}","slots":["board0592","board0371","board0223"]}',
  type: 'WALLET_SPEND_ACTION'
}
walletFactory: {
  wallet: Object [Alleged: SmartWallet self] {},
  actionCapData: {
    body: '#{"method":"executeOffer","offer":{"id":"joinGame1234","invitationSpec":{"instance":"$0.Alleged: SEVERED: InstanceHandle","publicInvitationMaker":"makeJoinInvitation","source":"contract"},"proposal":{"give":{"Price":{"brand":"$1.Alleged: SEVERED: ZDEFAULT brand","value":"+250000"}},"want":{"Places":{"brand":"$2.Alleged: SEVERED: Place brand","value":{"#tag":"copyBag","payload":[["Park Place","+1"],["Boardwalk","+1"]]}}}}}}',
    slots: [ 'board0592', 'board0371', 'board0223' ]
  }
}
wallet agoric1player1 starting executeOffer joinGame1234
join give {
  Price: { brand: Object [Alleged: ZDEFAULT brand] {}, value: 250000n }
} want Object [copyBag] {
  payload: [ [ 'Park Place', 1n ], [ 'Boardwalk', 1n ] ]
}
wallet agoric1player1 joinGame1234 seated
wallet agoric1player1 joinGame1234 offerResult string welcome to the game
wallet agoric1player1 offerStatus {
  id: 'joinGame1234',
  invitationSpec: {
    instance: Object [Alleged: InstanceHandle] {},
    publicInvitationMaker: 'makeJoinInvitation',
    source: 'contract'
  },
  proposal: { give: { Price: [Object] }, want: { Places: [Object] } },
  result: 'welcome to the game'
}
wallet agoric1player1 joinGame1234 numSatisfied 1
wallet agoric1player1 offerStatus {
  id: 'joinGame1234',
  invitationSpec: {
    instance: Object [Alleged: InstanceHandle] {},
    publicInvitationMaker: 'makeJoinInvitation',
    source: 'contract'
  },
  proposal: { give: { Price: [Object] }, want: { Places: [Object] } },
  result: 'welcome to the game',
  numWantsSatisfied: 1
}
wallet agoric1player1 offerStatus {
  id: 'joinGame1234',
  invitationSpec: {
    instance: Object [Alleged: InstanceHandle] {},
    publicInvitationMaker: 'makeJoinInvitation',
    source: 'contract'
  },
  proposal: { give: { Price: [Object] }, want: { Places: [Object] } },
  result: 'welcome to the game',
  numWantsSatisfied: 1,
  error: 'Error: cannot encode "[Alleged: Place brand]" as key: no ordinal'
}
wallet agoric1player1 offerStatus {
  id: 'joinGame1234',
  invitationSpec: {
    instance: Object [Alleged: InstanceHandle] {},
    publicInvitationMaker: 'makeJoinInvitation',
    source: 'contract'
  },
  proposal: { give: { Price: [Object] }, want: { Places: [Object] } },
  result: [
    {
      status: 'rejected',
      reason: [Error: "[Alleged: ZDEFAULT payment]" was not a live payment for brand "[Alleged: ZDEFAULT brand]". It could be a used-up payment, a payment for another brand, or it might not be a payment at all.
        at assertLivePayment (packages/ERTP/src/paymentLedger.js:238:11)
        at depositInternal (packages/ERTP/src/paymentLedger.js:265:5)
        at Object.deposit (packages/ERTP/src/purse.js:58:18)]
    }
  ],
  numWantsSatisfied: 1,
  error: 'Error: cannot encode "[Alleged: Place brand]" as key: no ordinal'
}
Temporary logging of sent error (Error#2)
Error#2: Object [Alleged: ZDEFAULT payment] {} was not a live payment for brand Object [Alleged: ZDEFAULT brand] {} . It could be a used-up payment, a payment for another brand, or it might not be a payment at all.
  at assertLivePayment (packages/ERTP/src/paymentLedger.js:238:11)
  at depositInternal (packages/ERTP/src/paymentLedger.js:265:5)
  at Object.deposit (packages/ERTP/src/purse.js:58:18)

Error#2 ERROR_NOTE: Sent as error:captp:far-zoeTest#20001
Temporary logging of sent error (RemoteError(error:captp:far-zoeTest#20001)#3)
RemoteError(error:captp:far-zoeTest#20001)#3: "[Alleged: ZDEFAULT payment]" was not a live payment for brand "[Alleged: ZDEFAULT brand]". It could be a used-up payment, a payment for another brand, or it might not be a payment at all.
  at Array.map (<anonymous>)
  at Array.map (<anonymous>)
  at Array.map (<anonymous>)
  at Array.map (<anonymous>)
  at Array.map (<anonymous>)
  at Array.map (<anonymous>)

RemoteError(error:captp:far-zoeTest#20001)#3 ERROR_NOTE: Sent as error:anon-marshal#10001
Temporary logging of sent error (Error#1)
Error#2 ERROR_NOTE: Sent as error:anon-marshal#10001
Temporary logging of sent error (Error#2)
Error#2 ERROR_NOTE: Sent as error:anon-marshal#10002
Temporary logging of sent error (Error#2)
Error#2 ERROR_NOTE: Sent as error:anon-marshal#10003
Temporary logging of sent error (Error#2)
Error#2 ERROR_NOTE: Sent as error:captp:far-zoeTest#20003
Temporary logging of sent error (Error#2)
@@@TODO: process queued payments with brand [object Alleged: Place brand]
wallet agoric1player1 OFFER ERROR: (Error#1)
Error#1: cannot encode Object [Alleged: Place brand] {} as key: no ordinal
  at encodeRemotable (packages/swingset-liveslots/src/collectionManager.js:284:13)
  at keyToDBKey (packages/swingset-liveslots/src/collectionManager.js:329:26)
  at Alleged: mapStore.set (packages/swingset-liveslots/src/collectionManager.js:431:21)
  at Object.providePurseForWellKnownBrand (.../smart-wallet/src/smartWallet.js:421:15)

Error#1 ERROR_NOTE: Sent as error:captp:far-zoeTest#20002
CapTP near-zoeTest exception: (RemoteError(error:captp:far-zoeTest#20002)#4)
RemoteError(error:captp:far-zoeTest#20002)#4: cannot encode "[Alleged: Place brand]" as key: no ordinal
Error: cannot encode "[Alleged: Place brand]" as key: no ordinal

  ✘ [fail]: trading in non-vbank assets @@TODO: fix, cannot encode "[Alleged: Place brand]" as key: no ordinal
    ℹ @@mutating agoricNames and vstorage may interfere with other tests
    ℹ @@TODO: wallet UI needs displayInfo. where to get it??
    ℹ agoric1player1 approves offer: joinGame1234
Error#2 ERROR_NOTE: Sent as error:captp:far-zoeTest#20004
Temporary logging of sent error (Error#2)
  ─

  trading in non-vbank assets
  @@TODO: fix, cannot encode "[Alleged: Place brand]" as key: no ordinal

  › packages/smart-wallet/test/test-addAsset.js:287:13

  ─

  1 test failed
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.

@dckc
Copy link
Member Author

dckc commented Jul 26, 2023

Happy path: trading in non-vbank asset: game real-estate NFTs

edit: updated Aug 1

The t.log() output of a happy path test tells a story that matches a prose version of the story.

  ✔ trading in non-vbank asset: game real-estate NFTs (2.7s)
    ℹ bootstrap: share agoricNames updates via vstorage RPC
    ℹ install game contract bundle with hash: 887dc94827e2e3a3c6fb19d8 ...
    ℹ deposit 10n IST into wallet of agoric1player1
    ℹ CoreEval script: started game contract Object @Alleged: InstanceHandle {}
    ℹ CoreEval script: share via agoricNames: Object @Alleged: Place brand {}
    ℹ game UI: ingested well-known brands: Object @Alleged: SEVERED: Place brand {}
    ℹ game UI: propose offer of 0.25IST for Park Place, Boardwalk
    ℹ agoric1player1 wallet UI: ingest well-known brands Object @Alleged: SEVERED: Place brand {}
    ℹ wallet: fungible display for Price Object @Alleged: SEVERED: ZDEFAULT brand {}
    ℹ wallet: NFT display for Places Object @Alleged: SEVERED: Place brand {}
    ℹ agoric1player1 walletUI: approve offer: joinGame1234
    ℹ agoric1player1 walletUI: broadcast signed message executeOffer
    ℹ joinGame1234 result: welcome to the game , payouts: Park Place, Boardwalk

formerly

a0fe29e, 86b8e65

  ✔ trading in non-vbank assets (2.3s)
    ℹ @@mutating agoricNames and vstorage may interfere with other tests
    ℹ @@TODO: wallet UI needs displayInfo. where to get it??
    ℹ agoric1player1 approves offer: joinGame1234

@dckc
Copy link
Member Author

dckc commented Jul 28, 2023

NameHub reverse lookup

re

I used .entries() and did the reverse lookup on the calling side.

@dckc dckc force-pushed the 7226-sw-nft-issuer branch from c18c083 to 98aebc5 Compare July 28, 2023 16:37
@dckc dckc changed the title test(smart-wallet): trading in non-vbank asset (WIP) feat(smart-wallet): trading in non-vbank asset (WIP) Jul 31, 2023
@dckc dckc force-pushed the 7226-sw-nft-issuer branch from 98aebc5 to c2c0250 Compare July 31, 2023 19:29
@dckc dckc force-pushed the 7226-sw-nft-issuer branch 2 times, most recently from 2b811eb to 1177b37 Compare August 1, 2023 18:03
@dckc dckc force-pushed the 7226-sw-nft-issuer branch from e82b349 to a711d5a Compare August 1, 2023 20:50
@dckc dckc changed the title feat(smart-wallet): trading in non-vbank asset (WIP) feat(smart-wallet): trading in well-known non-vbank assets Aug 1, 2023
@dckc dckc force-pushed the 7226-sw-nft-issuer branch 3 times, most recently from a7dbcd7 to 721cbbb Compare August 1, 2023 21:41
@dckc dckc marked this pull request as ready for review August 1, 2023 22:03
@dckc dckc requested a review from turadg August 1, 2023 22:03
Copy link
Member

@turadg turadg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

half-way through. will return soon for more

@@ -65,11 +65,12 @@ harden(assertCapData);
* @param {Map<string, string>} data
* @param {string} key
* @param {ReturnType<typeof import('@endo/marshal').makeMarshal>['fromCapData']} fromCapData
* @param {number} [index] index of the desired value in a deserialized stream cell
* @param {number} index index of the desired value in a deserialized stream cell
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@@ -17,6 +17,7 @@
},
"devDependencies": {
"@agoric/cosmic-proto": "^0.3.0",
"@endo/bundle-source": "^2.5.2",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: more of a fixup to the feat commit. if you end up rebasing, consider squashing

@@ -33,11 +42,14 @@ const bigIntReplacer = (_key, val) =>

const range = qty => [...Array(qty).keys()];

// We use test.serial() because
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

helpful comment.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also: exemplary test

@@ -24,7 +30,10 @@ test.before(async t => {
space0.produce.bankManager.resolve(bankManager);
return space0;
};
t.context = await makeDefaultTestContext(t, withBankManager);
const context = await makeDefaultTestContext(t, withBankManager);
const bfile = name => new URL(name, import.meta.url).pathname;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please provide in module scope per recent ocap/tests discussion

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Didn't we agree it's a matter of preference?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not my recollection. other people will have to maintain this code and import from context when they want to use bfile (and scratch their heads: "why, when this module is not exporting anything?" and then go maybe start another discussion and so on)

… but I'll demote this request to non-blocking

const bundles = {
game: await io.bundleSource(io.bfile('./gameAssetContract.js')),
centralSupply: await io.bundleSource(
io.bfile('../../vats/src/centralSupply.js'),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please use (a local copy of) importSpec so the packages dependency is apparent

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah. yes.

provideBrandToPurses() {
const brandToPurses = provideLazy(
walletPurses,
this.facets.helper,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this our idiom for "the key is this Exo"? is helper identity stable across restarts? and this isn't?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is indeed not a stable identity. The idiom is to use one of the facets. Oh... self seems more natural.

@@ -366,6 +428,87 @@ export const prepareSmartWallet = (baggage, shared) => {
},
});
},

provideBrandToPurses() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the caller needn't be concerned with provisioning. shouldn't this just be getBrandToPurses?

I'm also wondering whether it's worth putting it on the facet vs just inlining. What else would use this?

I'm genuinely not sure. It is a "helper" method so maybe it doesn't matter much.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm hoisting it to an ordinary function.

inlining obscured the code in providePurseForKnownBrand, but yeah, a facet method is overkill.

* @param {ERef<NameHub>} known - namehub with brand, issuer branches
* @returns {Promise<Purse | undefined>} undefined if brand is not well known
*/
async providePurseForKnownBrand(brand, known = shared.agoricNames) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto on abstracting provision

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The earlier case was more clear-cut, because there was no key argument. But here we do have a key -- the brand. If not here, where would we ever call something provideX? Perhaps only when we're also passing in a store?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, I think when the caller has the store in which it will be provided. if the caller just wants a thing and doesn't specify where to put it or how to make it, that's just a "get". Similar to how we don't put "memoize" in the name of functions that memoize.

*
* We current support only one NameHub, agoricNames, and
* hence one purse per brand. But we store an array of them
* to facilitate a transition decentralized introductions.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

transition to

*
* @param {Brand} brand
* @param {ERef<NameHub>} known - namehub with brand, issuer branches
* @returns {Promise<Purse | undefined>} undefined if brand is not well known
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the method call suggests the caller assumes the brand is well known. should this instead throw when that's not the case?

nit: strike "well" because "how known" is up to the caller

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

further down I see that you are concerning this function with "well known" because it takes a remote call that you wouldn't want to have to make before calling this and within the function too.

I think it would help to clarify that in the name, like e.g. providePurseIfKnownBrand. Reading the current name again I see it could be interpreted that way "provide purse for a known brand and undefined otherwise" but the "otherwise" is not hinted at.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, using If in the name is on target.

if (brandToPurses.has(brand)) {
const purses = brandToPurses.get(brand);
if (purses.length > 0) {
// For now, we have just 1, so use it.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// For now, we have just 1, so use it.
// UNTIL https://github.com/Agoric/agoric-sdk/issues/6126

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UNTIL doesn't seem to be documented like XXX, TODO, etc.. oversight?

}

/**
* Introduce an issuer to this wallet.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why "introduce" instead of "add"?

naming aside, this is adding an issuer even if it's already defined. it's also allowing multiple issuers per [brand,petname] composite key.

I see that's unlikely to happen because of how this is being called, but that makes me wonder why this is parameterized. if it were defined below const issuer = it could get the values it needs from the closure. you could even IIFE it without naming the function.

please simplify

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why "introduce" instead of "add"?

The idea was "introduce" as in: grant access to, not just "add something to a data structure".
That is: in the sense of Granovetter diagrams.

I was trying to think thru the flow where users decide which issuers to rely on. But I think this usage might be backwards. Alice the App introduces Bob the user/wallet to Carol the issuer. So the user/wallet is not doing the introducing. Also, Bob doesn't have a choice when Alice introduces Carol to him. And this is all about choosing whether to rely on an issuer.

Does acceptIssuer seem reasonable?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

perfectly

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if it were defined below const issuer = ...

yea, I'm inclined to just inline it there

* @param {Issuer} issuer
* @param {Brand} brand
*/
const mutualCheck = async (issuer, brand) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"assert" would make clear that this does nothing but throw. and the verb should come first.

Suggested change
const mutualCheck = async (issuer, brand) => {
const assertMutual = async (issuer, brand) => {

Comment on lines 501 to 478
try {
await mutualCheck(issuer, brand);
} catch (err) {
console.warn('issuer/brand mismatch', err);
return undefined;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

won't the error appear in the console upon throwing?

a failure of mutualCheck seems quite exceptional for a call to providePurseForKnownBrand. consider simplifying this to

Suggested change
try {
await mutualCheck(issuer, brand);
} catch (err) {
console.warn('issuer/brand mismatch', err);
return undefined;
}
await assertMutual(issuer, brand);

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code is called from depositFacet.receive(). So throwing here means dropping a payment on the floor.

Unless we try / catch in receive(). But assigning in a try/catch is ugly.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

from discussion: a match failure isn't so exceptional. Implies the brand isn't "known" (in a sane way) so returning undefined fits the description

*
* @param {Brand} brand
* @param {ERef<NameHub>} known - namehub with brand, issuer branches
* @returns {Promise<Purse | undefined>} undefined if brand is not well known
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

further down I see that you are concerning this function with "well known" because it takes a remote call that you wouldn't want to have to make before calling this and within the function too.

I think it would help to clarify that in the name, like e.g. providePurseIfKnownBrand. Reading the current name again I see it could be interpreted that way "provide purse for a known brand and undefined otherwise" but the "otherwise" is not hinted at.

appendToStoredArray(queues, brand, payment);
// @@NOTE: default 'nat' assetKind is not always right here
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this tech debt? what are the implications of 0n for the value of a non-nat Amount?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oops... @@ meant that I intended to deal with it before marking it rfr.

I wonder if we should throw here. Since we didn't deposit it into a purse, we don't actually have exclusive access to the payment. We're in a race with the caller to see who spends it first.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

from discussion: agree it should throw instead of empty. In general receive can fail. It does fail in this moment, even though it saves the payment to try again later.

@@ -24,7 +30,10 @@ test.before(async t => {
space0.produce.bankManager.resolve(bankManager);
return space0;
};
t.context = await makeDefaultTestContext(t, withBankManager);
const context = await makeDefaultTestContext(t, withBankManager);
const bfile = name => new URL(name, import.meta.url).pathname;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not my recollection. other people will have to maintain this code and import from context when they want to use bfile (and scratch their heads: "why, when this module is not exporting anything?" and then go maybe start another discussion and so on)

… but I'll demote this request to non-blocking

await signAndBroadcast(harden({ method: 'executeOffer', offer }));
},
});
bridgeKit.resolve(uiBridge);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why have bridgeKit instead of uiBridge?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I don't understand the question.

bridgeKit is a promise (etc.) for when uiBridge is available.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Understood. I'm questioning why they have different names. IIRC in the bootstrap we'd have the promise in the space have the name of the thing. The promise is for a uiBridge so I'd expect it to be named that. NBD

@@ -33,11 +42,14 @@ const bigIntReplacer = (_key, val) =>

const range = qty => [...Array(qty).keys()];

// We use test.serial() because
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also: exemplary test

appendToStoredArray(queues, brand, payment);
// @@NOTE: default 'nat' assetKind is not always right here
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does @@NOTE do anything useful?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's supposed to remind me to do something about it before marking the PR ready for review

dckc added 3 commits August 3, 2023 15:54
This performance optimization avoids all the work of getting an
invitation if there aren't sufficient funds for `proposal.give`.

fixes: #7098
WIP: note limitation on depositFacet assetKind

 - feat(smart-wallet): purses for well-known brands
   - use side table for new purse storage
   - issuer/brand mutual check
   - storage faciliteates transition
     to decentralized issuer introduction
   - take care with .init() vs. .set()!
     - diagnosis complicated by obscure "no ordinal" diagnostic
 - non-vbank asset test tells story in t.log
 - test: demonstrate how to share brand displayInfo
 - test: spend before receive non-vbank asset

build(smart-wallet): bundle-source devdep

SQUASHME: test: bundle handling, IST_UNIT, ...

SQUASHME: refactor based on review feedback

- XXX move? -> TODO
 - mutualCheck -> assertMutual with independent failures
 - hoist getBrandToPurses()
 - rename to getPurseIfKnownBrand()
 - inline addIssuer (aka acceptIssuer)
 - throw on mutualCheck failure

SQUASHME: xP

fixup receive throw

fixup getPurseIfKnownBrand
@turadg turadg force-pushed the 7226-sw-nft-issuer branch from 9ffd097 to 0db293c Compare August 3, 2023 19:55
@dckc dckc force-pushed the 7226-sw-nft-issuer branch from 0db293c to c2a8b98 Compare August 3, 2023 20:53
Copy link
Member

@turadg turadg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎉

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants