@river-build/sdk
Version:
For more details, visit the following resources:
235 lines • 12.9 kB
JavaScript
/**
* @group with-entitlements
*/
import { dlog } from '@river-build/dlog';
import { BigNumber, ethers } from 'ethers';
import { ETH_ADDRESS, LocalhostWeb3Provider } from '@river-build/web3';
import { makeRiverConfig } from '../../riverConfig';
import { Bot } from '../../sync-agent/utils/bot';
import { waitFor } from '../testUtils';
import { userIdFromAddress, makeUniqueChannelStreamId } from '../../id';
import { randomBytes } from 'crypto';
import { deepCopy } from 'ethers/lib/utils';
import { cloneDeep } from 'lodash';
const base_log = dlog('csb:test:transactions_Tip');
describe('transactions_Tip', () => {
const riverConfig = makeRiverConfig();
const bobIdentity = 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
let bob;
let alice;
let spaceId;
let defaultChannelId;
let messageId;
let aliceTokenId;
let dummyReceipt;
let dummyTipEvent;
let dummyTipEventCopy;
beforeAll(async () => {
// setup once
const log = base_log.extend('beforeAll');
log('start');
// fund wallets
await Promise.all([
bobIdentity.fundWallet(),
aliceIdentity.fundWallet(),
bobsOtherWalletProvider.fundWallet(),
]);
bob = await bobIdentity.makeSyncAgent();
alice = await aliceIdentity.makeSyncAgent();
// start agents
await Promise.all([
bob.start(),
alice.start(),
bob.riverConnection.spaceDapp.walletLink.linkWalletToRootKey(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
const { spaceId: sid, defaultChannelId: cid } = await bob.spaces.createSpace({ spaceName: 'BlastOff_Tip' }, bobIdentity.signer);
spaceId = sid;
defaultChannelId = cid;
await alice.spaces.joinSpace(spaceId, aliceIdentity.signer);
const channel = alice.spaces.getSpace(spaceId).getChannel(defaultChannelId);
const { eventId } = await channel.sendMessage('hello bob');
messageId = eventId;
log('bob and alice joined space', spaceId, defaultChannelId, messageId);
const aliceTokenId_ = await bob.riverConnection.spaceDapp.getTokenIdOfOwner(spaceId, aliceIdentity.rootWallet.address);
expect(aliceTokenId_).toBeDefined();
aliceTokenId = aliceTokenId_;
// dummy tip, to be used to test error cases
const tx = await bob.riverConnection.spaceDapp.tip({
spaceId,
tokenId: aliceTokenId,
currency: ETH_ADDRESS,
amount: 1000n,
messageId: messageId,
channelId: defaultChannelId,
receiver: aliceIdentity.rootWallet.address,
}, bobIdentity.signer);
dummyReceipt = await tx.wait(2);
dummyTipEvent = bob.riverConnection.spaceDapp.getTipEvent(spaceId, dummyReceipt, bobIdentity.rootWallet.address);
expect(dummyTipEvent).toBeDefined();
dummyTipEventCopy = deepCopy(dummyTipEvent);
expect(dummyTipEventCopy).toEqual(dummyTipEvent);
});
afterEach(() => {
expect(dummyTipEventCopy).toEqual(dummyTipEvent); // don't modify it please, it's used for error cases
});
afterAll(async () => {
await bob.stop();
await alice.stop();
});
test('addTip', async () => {
// a user should be able to upload a transaction that
// is a tip and is valid on chain
const tx = await bob.riverConnection.spaceDapp.tip({
spaceId,
tokenId: aliceTokenId,
currency: ETH_ADDRESS,
amount: 1000n,
messageId: messageId,
channelId: defaultChannelId,
receiver: aliceIdentity.rootWallet.address,
}, bobIdentity.signer);
const receipt = await tx.wait(2);
expect(receipt.from).toEqual(bobIdentity.rootWallet.address);
const tipEvent = bob.riverConnection.spaceDapp.getTipEvent(spaceId, receipt, bobIdentity.rootWallet.address);
expect(tipEvent).toBeDefined();
if (!tipEvent)
throw new Error('no tip event found');
await expect(bob.riverConnection.client.addTransaction_Tip(chainId, receipt, tipEvent, aliceIdentity.rootWallet.address)).resolves.not.toThrow();
});
test('bobSeesTipInUserStream', async () => {
// get the user "stream" that is being synced by bob
const stream = bob.riverConnection.client.stream(bob.riverConnection.client.userStreamId);
if (!stream)
throw new Error('no stream found');
const tipEvent = await waitFor(() => {
const isUserBlockchainTransaction = (e) => e.remoteEvent?.event.payload.case === 'userPayload' &&
e.remoteEvent.event.payload.value.content.case === 'blockchainTransaction';
const tipEvents = stream.view.timeline.filter(isUserBlockchainTransaction);
expect(tipEvents.length).toBeGreaterThan(0);
const tip = tipEvents[0];
// make it compile
if (!tip ||
tip.remoteEvent?.event.payload.value?.content.case !== 'blockchainTransaction')
throw new Error('no tip event found');
return tip.remoteEvent.event.payload.value.content.value;
});
expect(tipEvent?.receipt).toBeDefined();
// the view should have been updated with the tip
expect(stream.view.userContent.tipsSent[ETH_ADDRESS]).toEqual(1000n);
});
test('aliceSeesTipReceivedInUserStream', async () => {
// get the user "stream" that is being synced by alice
const stream = alice.riverConnection.client.stream(alice.riverConnection.client.userStreamId);
if (!stream)
throw new Error('no stream found');
const tipEvent = await waitFor(() => {
const isUserReceivedBlockchainTransaction = (e) => e.remoteEvent?.event.payload.case === 'userPayload' &&
e.remoteEvent.event.payload.value.content.case === 'receivedBlockchainTransaction';
const tipEvents = stream.view.timeline.filter(isUserReceivedBlockchainTransaction);
expect(tipEvents.length).toBeGreaterThan(0);
const tip = tipEvents[0];
// make it compile
if (!tip ||
tip.remoteEvent?.event.payload.value?.content.case !==
'receivedBlockchainTransaction')
throw new Error('no tip event found');
return tip.remoteEvent.event.payload.value.content.value;
});
if (!tipEvent)
throw new Error('no tip event found');
expect(tipEvent.transaction?.receipt).toBeDefined();
expect(tipEvent?.transaction?.content?.case).toEqual('tip');
// the view should have been updated with the tip
expect(stream.view.userContent.tipsReceived[ETH_ADDRESS]).toEqual(1000n);
});
test('bobSeesOnMessageInChannel', async () => {
// get the channel "stream" that is being synced by bob
const stream = bob.riverConnection.client.stream(defaultChannelId);
if (!stream)
throw new Error('no stream found');
const tipEvent = await waitFor(() => {
const isMemberBlockchainTransaction = (e) => e.remoteEvent?.event.payload.case === 'memberPayload' &&
e.remoteEvent.event.payload.value.content.case === 'memberBlockchainTransaction';
const tipEvents = stream.view.timeline.filter(isMemberBlockchainTransaction);
expect(tipEvents.length).toBeGreaterThan(0);
const tip = tipEvents[0];
// make it compile
if (!tip ||
tip.remoteEvent?.event.payload.value?.content.case !== 'memberBlockchainTransaction')
throw new Error('no tip event found');
return tip.remoteEvent.event.payload.value.content.value;
});
expect(userIdFromAddress(tipEvent.fromUserAddress)).toEqual(bobIdentity.rootWallet.address);
expect(stream.view.membershipContent.tips[ETH_ADDRESS]).toEqual(1000n);
});
test('cantAddTipWithBadChannelId', async () => {
const event = cloneDeep(dummyTipEvent);
event.channelId = makeUniqueChannelStreamId(spaceId);
await expect(bob.riverConnection.client.addTransaction_Tip(chainId, dummyReceipt, event, aliceIdentity.rootWallet.address, { disableTags: true })).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, aliceIdentity.rootWallet.address)).rejects.toThrow('matching tip event not found in receipt logs');
});
test('cantAddTipWithBadSender', async () => {
const event = cloneDeep(dummyTipEvent);
event.sender = aliceIdentity.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('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, 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, 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');
});
test('bobSnapshot', async () => {
// force a snapshot of the user "stream" that is being synced by bob
await bob.riverConnection.client.debugForceMakeMiniblock(bob.riverConnection.client.userStreamId, { forceSnapshot: true });
// refetch the stream using getStream, make sure it parses the snapshot correctly
const stream = await bob.riverConnection.client.getStream(bob.riverConnection.client.userStreamId);
expect(stream.userContent.tipsSent[ETH_ADDRESS]).toEqual(1000n);
expect(stream.userContent.tipsReceived[ETH_ADDRESS]).toBeUndefined();
});
test('aliceSnapshot', async () => {
// force a snapshot of the user "stream" that is being synced by alice
await alice.riverConnection.client.debugForceMakeMiniblock(alice.riverConnection.client.userStreamId, { forceSnapshot: true });
// refetch the gtream using getStream, make sure it parses the snapshot correctly
const stream = await alice.riverConnection.client.getStream(alice.riverConnection.client.userStreamId);
expect(stream.userContent.tipsReceived[ETH_ADDRESS]).toEqual(1000n);
expect(stream.userContent.tipsSent[ETH_ADDRESS]).toBeUndefined();
});
test('channelSnapshot', async () => {
// force a snapshot of the channel "stream" that is being synced by bob
await bob.riverConnection.client.debugForceMakeMiniblock(defaultChannelId, {
forceSnapshot: true,
});
// refetch the stream using getStream, make sure it parses the snapshot correctly
const stream = await bob.riverConnection.client.getStream(defaultChannelId);
if (!stream)
throw new Error('no stream found');
expect(stream.membershipContent.tips[ETH_ADDRESS]).toEqual(1000n);
});
});
//# sourceMappingURL=transactions_Tip.test.js.map