Skip to content

Commit

Permalink
Tip events, properly handle the to user address
Browse files Browse the repository at this point in the history
The sender and receiver of the tip are the wallets that hold the space nfts, not the root wallet addresses. So we can’t assume that the receiver will have a user stream. We need to specify the to user address in the metadata and verify on appending a received transaction that this actually belongs to the user.
  • Loading branch information
texuf committed Dec 22, 2024
1 parent 487af77 commit eebc46b
Show file tree
Hide file tree
Showing 7 changed files with 743 additions and 572 deletions.
4 changes: 2 additions & 2 deletions core/node/auth/auth_impl.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,12 +85,12 @@ func NewChainAuthArgsForIsSpaceMember(spaceId shared.StreamId, userId string) *C
}

func NewChainAuthArgsForIsWalletLinked(
userId []byte,
userAddress []byte,
walletAddress []byte,
) *ChainAuthArgs {
return &ChainAuthArgs{
kind: chainAuthKindIsWalletLinked,
principal: common.BytesToAddress(userId),
principal: common.BytesToAddress(userAddress),
walletAddress: common.BytesToAddress(walletAddress),
}
}
Expand Down
1,135 changes: 606 additions & 529 deletions core/node/protocol/protocol.pb.go

Large diffs are not rendered by default.

52 changes: 38 additions & 14 deletions core/node/rules/can_add_event.go
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,7 @@ func (params *aeParams) canAddUserPayload(payload *StreamEvent_UserPayload) rule
return aeBuilder().
check(ru.params.creatorIsValidNode).
check(ru.validReceivedBlockchainTransaction_IsUnique).
requireChainAuth(ru.receivedBlockchainTransaction_ChainAuth).
requireParentEvent(ru.parentEventForReceivedBlockchainTransaction)
default:
return aeBuilder().
Expand Down Expand Up @@ -621,12 +622,12 @@ func (ru *aeMemberBlockchainTransactionRules) validMemberBlockchainTransaction_R
if err != nil {
return false, err
}
err = checkIsMember(ru.params, content.Tip.GetReceiver())
err = checkIsMember(ru.params, content.Tip.GetEvent().GetReceiver())
if err != nil {
return false, err
}
// we need a ref event id
if content.Tip.GetMessageId() == nil {
if content.Tip.GetEvent().GetMessageId() == nil {
return false, RiverError(Err_INVALID_ARGUMENT, "tip transaction message id is nil")
}
return true, nil
Expand Down Expand Up @@ -726,25 +727,25 @@ func (ru *aeBlockchainTransactionRules) validBlockchainTransaction_CheckReceiptM
if err != nil {
continue // not a tip
}
if tipEvent.TokenId.Cmp(big.NewInt(int64(content.Tip.GetTokenId()))) != 0 {
if tipEvent.TokenId.Cmp(big.NewInt(int64(content.Tip.GetEvent().GetTokenId()))) != 0 {
continue
}
if !bytes.Equal(tipEvent.Currency[:], content.Tip.GetCurrency()) {
if !bytes.Equal(tipEvent.Currency[:], content.Tip.GetEvent().GetCurrency()) {
continue
}
if !bytes.Equal(tipEvent.Sender[:], content.Tip.GetSender()) {
if !bytes.Equal(tipEvent.Sender[:], content.Tip.GetEvent().GetSender()) {
continue
}
if !bytes.Equal(tipEvent.Receiver[:], content.Tip.GetReceiver()) {
if !bytes.Equal(tipEvent.Receiver[:], content.Tip.GetEvent().GetReceiver()) {
continue
}
if tipEvent.Amount.Cmp(big.NewInt(int64(content.Tip.GetAmount()))) != 0 {
if tipEvent.Amount.Cmp(big.NewInt(int64(content.Tip.GetEvent().GetAmount()))) != 0 {
continue
}
if !bytes.Equal(tipEvent.MessageId[:], content.Tip.GetMessageId()) {
if !bytes.Equal(tipEvent.MessageId[:], content.Tip.GetEvent().GetMessageId()) {
continue
}
if !bytes.Equal(tipEvent.ChannelId[:], content.Tip.GetChannelId()) {
if !bytes.Equal(tipEvent.ChannelId[:], content.Tip.GetEvent().GetChannelId()) {
continue
}
// match found
Expand All @@ -764,6 +765,29 @@ func (ru *aeBlockchainTransactionRules) validBlockchainTransaction_CheckReceiptM
}
}

func (ru *aeReceivedBlockchainTransactionRules) receivedBlockchainTransaction_ChainAuth() (*auth.ChainAuthArgs, error) {
transaction := ru.receivedTransaction.Transaction
if transaction == nil {
return nil, RiverError(Err_INVALID_ARGUMENT, "transaction is nil")
}

switch content := transaction.Content.(type) {
case nil:
return nil, nil
case *BlockchainTransaction_Tip_:
userAddress, err := shared.GetUserAddressFromStreamId(*ru.params.streamView.StreamId())
if err != nil {
return nil, err
}
return auth.NewChainAuthArgsForIsWalletLinked(
userAddress.Bytes(),
content.Tip.GetToUserAddress(),
), nil
default:
return nil, RiverError(Err_INVALID_ARGUMENT, "unknown received transaction kind for chain auth", "kind", ru.receivedTransaction.Kind)
}
}

func (ru *aeReceivedBlockchainTransactionRules) parentEventForReceivedBlockchainTransaction() (*DerivedEvent, error) {
transaction := ru.receivedTransaction.Transaction
if transaction == nil {
Expand All @@ -780,11 +804,11 @@ func (ru *aeReceivedBlockchainTransactionRules) parentEventForReceivedBlockchain
if !ok {
return nil, RiverError(Err_INVALID_ARGUMENT, "content is not a tip")
}
if content.Tip.GetChannelId() == nil {
if content.Tip.GetEvent().GetChannelId() == nil {
return nil, RiverError(Err_INVALID_ARGUMENT, "transaction channel id is nil")
}
// convert to stream id
streamId, err := shared.StreamIdFromBytes(content.Tip.GetChannelId())
streamId, err := shared.StreamIdFromBytes(content.Tip.GetEvent().GetChannelId())
if err != nil {
return nil, err
}
Expand All @@ -808,11 +832,11 @@ func (ru *aeBlockchainTransactionRules) parentEventForBlockchainTransaction() (*
return nil, nil
case *BlockchainTransaction_Tip_:
// forward a "tip received" event to the user stream of the toUserAddress
userStreamId, err := shared.UserStreamIdFromBytes(content.Tip.GetReceiver())
userStreamId, err := shared.UserStreamIdFromBytes(content.Tip.GetEvent().GetReceiver())
if err != nil {
return nil, err
}
toStreamId, err := shared.StreamIdFromBytes(content.Tip.GetChannelId())
toStreamId, err := shared.StreamIdFromBytes(content.Tip.GetEvent().GetChannelId())
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -866,7 +890,7 @@ func (ru *aeBlockchainTransactionRules) blockchainTransaction_ChainAuth() (*auth
// as specified in the tip content and verified against the logs in blockchainTransaction_CheckReceiptMetadata
return auth.NewChainAuthArgsForIsWalletLinked(
ru.params.parsedEvent.Event.CreatorAddress,
content.Tip.GetSender(),
content.Tip.GetEvent().GetSender(),
), nil
default:
return nil, RiverError(
Expand Down
18 changes: 11 additions & 7 deletions packages/sdk/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1934,17 +1934,21 @@ export class Client
chainId: number,
receipt: ContractReceipt,
event: TipEventObject,
toUserId: string,
): Promise<{ eventId: string }> {
return this.addTransaction(chainId, receipt, {
case: 'tip',
value: {
tokenId: event.tokenId.toBigInt(),
currency: bin_fromHexString(event.currency),
sender: addressFromUserId(event.sender),
receiver: addressFromUserId(event.receiver),
amount: event.amount.toBigInt(),
messageId: bin_fromHexString(event.messageId),
channelId: streamIdAsBytes(event.channelId),
event: {
tokenId: event.tokenId.toBigInt(),
currency: bin_fromHexString(event.currency),
sender: addressFromUserId(event.sender),
receiver: addressFromUserId(event.receiver),
amount: event.amount.toBigInt(),
messageId: bin_fromHexString(event.messageId),
channelId: streamIdAsBytes(event.channelId),
},
toUserAddress: addressFromUserId(toUserId),
},
})
}
Expand Down
9 changes: 6 additions & 3 deletions packages/sdk/src/sync-agent/timeline/models/timelineEvent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -988,11 +988,14 @@ function getFallbackContent_BlockchainTransaction(
}
switch (transaction.content.case) {
case 'tip':
if (!transaction.content.value?.event) {
return '??'
}
return `kind: ${transaction.content.case} messageId: ${bin_toHexString(
transaction.content.value.messageId,
transaction.content.value.event.messageId,
)} receiver: ${bin_toHexString(
transaction.content.value.receiver,
)} amount: ${transaction.content.value.amount.toString()}`
transaction.content.value.event.receiver,
)} amount: ${transaction.content.value.event.amount.toString()}`
default:
return `kind: ${transaction.content.case ?? 'unspecified'}`
}
Expand Down
76 changes: 68 additions & 8 deletions packages/sdk/src/tests/multi/transactions_Tip.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,13 @@ const base_log = dlog('csb:test:transactions_Tip')
describe('transactions_Tip', () => {
const riverConfig = makeRiverConfig()
const bobIdentity = new Bot(undefined, riverConfig)
const aliceIdentity = new Bot(undefined, riverConfig)
const bobsOtherWallet = ethers.Wallet.createRandom()
const bobsOtherWalletProvider = new LocalhostWeb3Provider(
riverConfig.base.rpcUrl,
bobsOtherWallet,
)
const aliceIdentity = new Bot(undefined, riverConfig)
const alicesOtherWallet = ethers.Wallet.createRandom()
const chainId = riverConfig.base.chainConfig.chainId

// updated once and shared between tests
Expand Down Expand Up @@ -64,6 +65,10 @@ describe('transactions_Tip', () => {
bobIdentity.signer,
bobsOtherWallet,
),
alice.riverConnection.spaceDapp.walletLink.linkWalletToRootKey(
aliceIdentity.signer,
alicesOtherWallet,
),
])

// before they can do anything on river, they need to be in a space
Expand Down Expand Up @@ -137,7 +142,12 @@ describe('transactions_Tip', () => {
)
expect(tipEvent).toBeDefined()
await expect(
bob.riverConnection.client!.addTransaction_Tip(chainId, receipt, tipEvent!),
bob.riverConnection.client!.addTransaction_Tip(
chainId,
receipt,
tipEvent!,
aliceIdentity.rootWallet.address,
),
).resolves.not.toThrow()
})

Expand Down Expand Up @@ -213,39 +223,89 @@ describe('transactions_Tip', () => {
const event = cloneDeep(dummyTipEvent)
event.channelId = makeUniqueChannelStreamId(spaceId)
await expect(
bob.riverConnection.client!.addTransaction_Tip(chainId, dummyReceipt, event),
bob.riverConnection.client!.addTransaction_Tip(
chainId,
dummyReceipt,
event,
aliceIdentity.rootWallet.address,
),
).rejects.toThrow('matching tip event not found in receipt logs')
})

test('cantAddTipWithBadMessageId', async () => {
const event = cloneDeep(dummyTipEvent)
event.messageId = randomBytes(32).toString('hex')
await expect(
bob.riverConnection.client!.addTransaction_Tip(chainId, dummyReceipt, event),
bob.riverConnection.client!.addTransaction_Tip(
chainId,
dummyReceipt,
event,
aliceIdentity.rootWallet.address,
),
).rejects.toThrow('matching tip event not found in receipt logs')
})

test('cantAddTipWithBadFromUserAddress', async () => {
test('cantAddTipWithBadSender', async () => {
const event = cloneDeep(dummyTipEvent)
event.sender = aliceIdentity.rootWallet.address
await expect(
bob.riverConnection.client!.addTransaction_Tip(chainId, dummyReceipt, event),
bob.riverConnection.client!.addTransaction_Tip(
chainId,
dummyReceipt,
event,
aliceIdentity.rootWallet.address,
),
).rejects.toThrow('matching tip event not found in receipt logs')
})

test('cantAddTipWithBadReceiver', async () => {
const event = cloneDeep(dummyTipEvent)
event.receiver = bobIdentity.rootWallet.address
await expect(
bob.riverConnection.client!.addTransaction_Tip(
chainId,
dummyReceipt,
event,
aliceIdentity.rootWallet.address,
),
).rejects.toThrow('matching tip event not found in receipt logs')
})

test('cantAddTipWithBadAmount', async () => {
const event = cloneDeep(dummyTipEvent)
event.amount = BigNumber.from(10000000n)
await expect(
bob.riverConnection.client!.addTransaction_Tip(chainId, dummyReceipt, event),
bob.riverConnection.client!.addTransaction_Tip(
chainId,
dummyReceipt,
event,
aliceIdentity.rootWallet.address,
),
).rejects.toThrow('matching tip event not found in receipt logs')
})

test('cantAddTipWithBadCurrency', async () => {
const event = cloneDeep(dummyTipEvent)
event.currency = '0x0000000000000000000000000000000000000000'
await expect(
bob.riverConnection.client!.addTransaction_Tip(chainId, dummyReceipt, event),
bob.riverConnection.client!.addTransaction_Tip(
chainId,
dummyReceipt,
event,
aliceIdentity.rootWallet.address,
),
).rejects.toThrow('matching tip event not found in receipt logs')
})

test('cantAddTipWithBadToUserAddress', async () => {
const event = cloneDeep(dummyTipEvent)
await expect(
bob.riverConnection.client!.addTransaction_Tip(
chainId,
dummyReceipt,
event,
bobIdentity.rootWallet.address,
),
).rejects.toThrow('IsEntitled failed')
})
})
21 changes: 12 additions & 9 deletions protocol/protocol.proto
Original file line number Diff line number Diff line change
Expand Up @@ -611,19 +611,22 @@ message Snapshot {
}

message BlockchainTransaction {
// metadata for tip transactions
message Tip {
uint64 token_id = 1;
bytes currency = 2;
bytes sender = 3;
bytes receiver = 4;
uint64 amount = 5;
bytes message_id = 6;
bytes channel_id = 7;
message Event {
uint64 token_id = 1;
bytes currency = 2;
bytes sender = 3; // wallet that sent funds
bytes receiver = 4; // wallet that received funds
uint64 amount = 5;
bytes message_id = 6;
bytes channel_id = 7;
}
Event event = 1; // event emitted by the tipping facet
bytes toUserAddress = 2; // user that received funds
}

// required fields
BlockchainTransactionReceipt receipt = 1;

// optional metadata to be verified by the node
oneof content {
Tip tip = 101;
Expand Down

0 comments on commit eebc46b

Please sign in to comment.