Skip to content
This repository was archived by the owner on Dec 21, 2021. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
b10fe81
ETH-81: Expose transportSignatures for an alternative withdraw flow
Mar 30, 2021
99f2abf
make eslint happy
Mar 30, 2021
5e0a208
Update README with two-(three?)step withdraw
Mar 30, 2021
b52a2ec
fix tables
Mar 31, 2021
5091cf3
Merge branch 'master' into ETH-81-expose-signature-transport
Apr 15, 2021
42d99e8
transportSignature -> payForTransport
Apr 16, 2021
de6bbec
Added tests for each of the withdraw patterns documented in README
Apr 16, 2021
501a15b
added type
Apr 19, 2021
33b1715
fix timing issue
Apr 19, 2021
ad297a4
added todo ticket number, cleanup
Apr 19, 2021
02d4dbb
fix broken type test
Apr 20, 2021
01840c3
Better error message
Apr 21, 2021
8ab7940
tweak timings
Apr 22, 2021
178a8e1
fix the bridge-sponsored withdraw case
Apr 22, 2021
1e6b1a6
cleanup
Apr 22, 2021
cdfedc2
longer timeout
Apr 22, 2021
13b20b3
timeout tweaks
Apr 23, 2021
670377c
Improve logging
Apr 23, 2021
acc7157
these types are in public APIs
Apr 24, 2021
388fd70
typo fix
Apr 26, 2021
a502204
make lint happy
Apr 26, 2021
f4567bb
Merge branch 'master' into ETH-81-expose-signature-transport
Apr 26, 2021
8db8d5b
clean up old merged PR 241
Apr 26, 2021
aeded55
small fix
Apr 26, 2021
fbed773
deterministic test wallet generation
Apr 27, 2021
5a274bf
Use whitelist with bridge-sponsoring
Apr 27, 2021
8ebafc3
Fix bridge, refactor out getBalance argument
Apr 27, 2021
f21cd2b
Fix bridge whitelisting inside the test
Apr 27, 2021
00a5884
Merge branch 'master' into ETH-81-expose-signature-transport
Apr 27, 2021
aebff64
fix ETH-81: engine-and-editor -> core-api
Apr 27, 2021
b0a6152
Remove the rest of Date.now() pseudo-ids
Apr 28, 2021
9f32fda
logging overhaul
Apr 30, 2021
893409a
freeWithdraw -> !payForTransport
Apr 30, 2021
c0105ca
Parallelized some blockchain operations that take an HTTP call
Apr 30, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/test-code.yml
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ jobs:
- name: Start Streamr Docker Stack
uses: streamr-dev/streamr-docker-dev-action@v1.0.0-alpha.3
with:
services-to-start: "mysql redis engine-and-editor cassandra parity-node0 parity-sidechain-node0 bridge broker-node-no-storage-1 broker-node-no-storage-2 broker-node-storage-1 nginx smtp"
services-to-start: "mysql redis core-api cassandra parity-node0 parity-sidechain-node0 bridge broker-node-no-storage-1 broker-node-no-storage-2 broker-node-storage-1 nginx smtp"
- name: Run Test
run: npm run $TEST_NAME

Expand All @@ -131,7 +131,7 @@ jobs:
- name: Start Streamr Docker Stack
uses: streamr-dev/streamr-docker-dev-action@v1.0.0-alpha.3
with:
services-to-start: "mysql redis engine-and-editor cassandra parity-node0 parity-sidechain-node0 bridge broker-node-no-storage-1 broker-node-no-storage-2 broker-node-storage-1 nginx smtp"
services-to-start: "mysql redis core-api cassandra parity-node0 parity-sidechain-node0 bridge broker-node-no-storage-1 broker-node-no-storage-2 broker-node-storage-1 nginx smtp"
- uses: nick-invision/retry@v2
name: Run Test
with:
Expand Down
72 changes: 51 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -360,11 +360,11 @@ const dataUnion = client.getDataUnion(dataUnionAddress)
<!-- This stuff REALLY isn't for those who use our infrastructure, neither DU admins nor DU client devs. It's only relevant if you're setting up your own sidechain.
These DataUnion-specific options can be given to `new StreamrClient` options:

| Property | Default | Description |
| :---------------------------------- | :----------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------- |
| dataUnion.minimumWithdrawTokenWei | 1000000 | Threshold value set in AMB configs, smallest token amount that can pass over the bridge |
| dataUnion.freeWithdraw | false | true = someone else pays for the gas when transporting the withdraw tx to mainnet |
| | | false = client does the transport as self-service and pays the mainnet gas costs |
| Property | Default | Description |
| :---------------------------------- | :----------------------------------------------------- | :----------------------------------------------------------------------------------------- |
| dataUnion.minimumWithdrawTokenWei | 1000000 | Threshold value set in AMB configs, smallest token amount that can pass over the bridge |
| dataUnion.payForTransport | true | true = client does the transport as self-service and pays the mainnet gas costs |
| | | false = someone else pays for the gas when transporting the withdraw tx to mainnet |
-->

### Admin Functions
Expand All @@ -375,8 +375,10 @@ These DataUnion-specific options can be given to `new StreamrClient` options:
| setAdminFee(newFeeFraction) | Transaction receipt | `newFeeFraction` is a `Number` between 0.0 and 1.0 (inclusive) |
| addMembers(memberAddressList) | Transaction receipt | Add members |
| removeMembers(memberAddressList) | Transaction receipt | Remove members from Data Union |
| withdrawAllToMember(memberAddress\[, [options](#withdraw-options)\]) | Transaction receipt | Send all withdrawable earnings to the member's address |
| withdrawAllToSigned(memberAddress, recipientAddress, signature\[, [options](#withdraw-options)\]) | Transaction receipt | Send all withdrawable earnings to the address signed off by the member (see [example below](#member-functions)) |
| withdrawAllToMember(memberAddress\[, [options](#withdraw-options)\]) | Transaction receipt `*` | Send all withdrawable earnings to the member's address |
| withdrawAllToSigned(memberAddress, recipientAddress, signature\[, [options](#withdraw-options)\]) | Transaction receipt `*` | Send all withdrawable earnings to the address signed off by the member (see [example below](#member-functions)) |

`*` The return value type may vary depending on [the given options](#withdraw-options) that describe the use case.

Here's how to deploy a Data Union contract with 30% Admin fee and add some members:

Expand All @@ -399,14 +401,17 @@ const receipt = await dataUnion.addMembers([

### Member functions

| Name | Returns | Description |
| :---------------------------------------------------------------- | :------------------ | :-------------------------------------------------------------------------- |
| join(\[secret]) | JoinRequest | Join the Data Union (if a valid secret is given, the promise waits until the automatic join request has been processed) |
| isMember(memberAddress) | boolean | |
| withdrawAll(\[[options](#withdraw-options)\]) | Transaction receipt | Withdraw funds from Data Union |
| withdrawAllTo(recipientAddress\[, [options](#withdraw-options)\]) | Transaction receipt | Donate/move your earnings to recipientAddress instead of your memberAddress |
| signWithdrawAllTo(recipientAddress) | Signature (string) | Signature that can be used to withdraw all available tokens to given recipientAddress |
| signWithdrawAmountTo(recipientAddress, amountTokenWei) | Signature (string) | Signature that can be used to withdraw a specific amount of tokens to given recipientAddress |
| Name | Returns | Description |
| :-------------------------------------------------------------------- | :------------------------ | :-------------------------------------------------------------------------- |
| join(\[secret]) | JoinRequest | Join the Data Union (if a valid secret is given, the promise waits until the automatic join request has been processed) |
| isMember(memberAddress) | boolean | |
| withdrawAll(\[[options](#withdraw-options)\]) | Transaction receipt `*` | Withdraw funds from Data Union |
| withdrawAllTo(recipientAddress\[, [options](#withdraw-options)\]) | Transaction receipt `*` | Donate/move your earnings to recipientAddress instead of your memberAddress |
| signWithdrawAllTo(recipientAddress) | Signature (string) | Signature that can be used to withdraw all available tokens to given recipientAddress |
| signWithdrawAmountTo(recipientAddress, amountTokenWei) | Signature (string) | Signature that can be used to withdraw a specific amount of tokens to given recipientAddress |
| transportMessage(messageHash[, pollingIntervalMs[, retryTimeoutMs]]) | Transaction receipt | Send the mainnet transaction to withdraw tokens from the sidechain |

`*` The return value type may vary depending on [the given options](#withdraw-options) that describe the use case.

Here's an example on how to sign off on a withdraw to (any) recipientAddress (NOTE: this requires no gas!)

Expand Down Expand Up @@ -434,6 +439,15 @@ const dataUnion = client.getDataUnion(dataUnionAddress)
const receipt = await dataUnion.withdrawAllToSigned(memberAddress, recipientAddress, signature)
```

The `messageHash` argument to `transportMessage` will come from the withdraw function with the specific options. The following is equivalent to the above withdraw line:
```js
const messageHash = await dataUnion.withdrawAllToSigned(memberAddress, recipientAddress, signature, {
payForTransport: false,
waitUntilTransportIsComplete: false,
}) // only pay for sidechain gas
const receipt = await dataUnion.transportMessage(messageHash) // only pay for mainnet gas
```

### Query functions

These are available for everyone and anyone, to query publicly available info from a Data Union:
Expand All @@ -460,13 +474,29 @@ const withdrawableWei = await dataUnion.getWithdrawableEarnings(memberAddress)

The functions `withdrawAll`, `withdrawAllTo`, `withdrawAllToMember`, `withdrawAllToSigned` all can take an extra "options" argument. It's an object that can contain the following parameters:

| Name | Default | Description |
| :---------------- | :-------------------- | :---------------------------------------------------------------------------------- |
| sendToMainnet | true | Whether to send the withdrawn DATA tokens to mainnet address (or sidechain address) |
| pollingIntervalMs | 1000 (1&nbsp;second) | How often requests are sent to find out if the withdraw has completed |
| retryTimeoutMs | 60000 (1&nbsp;minute) | When to give up when waiting for the withdraw to complete |
| Name | Default | Description |
| :---------------- | :-------------------- | :---------------------------------------------------------------------------------- |
| sendToMainnet | true | Whether to send the withdrawn DATA tokens to mainnet address (or sidechain address) |
| payForTransport | true | Whether to pay for the withdraw transaction signature transport to mainnet over the bridge |
| waitUntilTransportIsComplete | true | Whether to wait until the withdrawn DATA tokens are visible in mainnet |
| pollingIntervalMs | 1000 (1&nbsp;second) | How often requests are sent to find out if the withdraw has completed |
| retryTimeoutMs | 60000 (1&nbsp;minute) | When to give up when waiting for the withdraw to complete |

These withdraw transactions are sent to the sidechain, so gas price shouldn't be manually set (fees will hopefully stay very low),
but a little bit of [sidechain native token](https://www.xdaichain.com/for-users/get-xdai-tokens) is nonetheless required.

The return values from the withdraw functions also depend on the options.

If `sendToMainnet: false`, other options don't apply at all, and **sidechain transaction receipt** is returned as soon as the withdraw transaction is done. This should be fairly quick in the sidechain.

The use cases corresponding to the different combinations of the boolean flags:

These withdraw transactions are sent to the sidechain, so gas price shouldn't be manually set (fees will hopefully stay very low), but a little bit of [sidechain native token](https://www.xdaichain.com/for-users/get-xdai-tokens) is nonetheless required.
| `transport` | `wait` | Returns | Effect |
| :---------- | :------ | :------ | :----- |
| `true` | `true` | Transaction receipt | *(default)* Self-service bridge to mainnet, client pays for mainnet gas |
| `true` | `false` | Transaction receipt | Self-service bridge to mainnet (but **skip** the wait that double-checks the withdraw succeeded and tokens arrived to destination) |
| `false` | `true` | `null` | Someone else pays for the mainnet gas automatically, e.g. the bridge operator (in this case the transaction receipt can't be returned) |
| `false` | `false` | AMB message hash | Someone else pays for the mainnet gas, but we need to give them the message hash first |

### Deployment options

Expand Down
4 changes: 2 additions & 2 deletions src/Config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export type StrictStreamrClientOptions = {
* otherwise the client does the transport as self-service and pays the mainnet gas costs
*/
minimumWithdrawTokenWei: BigNumber|number|string
freeWithdraw: boolean
payForTransport: boolean
factoryMainnetAddress: EthereumAddress
factorySidechainAddress: EthereumAddress
templateMainnetAddress: EthereumAddress
Expand Down Expand Up @@ -137,7 +137,7 @@ export const STREAM_CLIENT_DEFAULTS: StrictStreamrClientOptions = {
tokenSidechainAddress: '0xE4a2620edE1058D61BEe5F45F6414314fdf10548',
dataUnion: {
minimumWithdrawTokenWei: '1000000',
freeWithdraw: false,
payForTransport: true,
factoryMainnetAddress: '0x7d55f9981d4E10A193314E001b96f72FCc901e40',
factorySidechainAddress: '0x1b55587Beea0b5Bc96Bb2ADa56bD692870522e9f',
templateMainnetAddress: '0x5FE790E3751dd775Cb92e9086Acd34a2adeB8C7b',
Expand Down
47 changes: 4 additions & 43 deletions src/dataunion/Contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { BigNumber } from '@ethersproject/bignumber'
import StreamrEthereum from '../Ethereum'
import { StreamrClient } from '../StreamrClient'

const log = debug('StreamrClient::DataUnion')
const log = debug('StreamrClient::Contracts')

function validateAddress(name: string, address: EthereumAddress) {
if (!isAddress(address)) {
Expand Down Expand Up @@ -137,7 +137,7 @@ export class Contracts {
}

// move signatures from sidechain to mainnet
async transportSignaturesForMessage(messageHash: string) {
async transportSignaturesForMessage(messageHash: string): Promise<ContractReceipt | null> {
const sidechainAmb = await this.getSidechainAmb()
const message = await sidechainAmb.message(messageHash)
const messageId = '0x' + message.substr(2, 64)
Expand All @@ -149,7 +149,7 @@ export class Contracts {

const [vArray, rArray, sArray]: Todo = [[], [], []]
signatures.forEach((signature: string, i) => {
log(` Signature ${i}: ${signature} (len=${signature.length}=${signature.length / 2 - 1} bytes)`)
log(` Signature ${i}: ${signature} (len=${signature.length} = ${signature.length / 2 - 1} bytes)`)
rArray.push(signature.substr(2, 64))
sArray.push(signature.substr(66, 64))
vArray.push(signature.substr(130, 2))
Expand All @@ -171,7 +171,7 @@ export class Contracts {
const alreadyProcessed = await mainnetAmb.relayedMessages(messageId)
if (alreadyProcessed) {
log(`WARNING: Tried to transport signatures but they have already been transported (Message ${messageId} has already been processed)`)
log('This could happen if freeWithdraw=false (attempt self-service), but bridge actually paid before your client')
log('This could happen if bridge paid for transport before your client.')
return null
}

Expand Down Expand Up @@ -218,45 +218,6 @@ export class Contracts {
return trAMB
}

async transportSignaturesForTransaction(tr: ContractReceipt, options: { pollingIntervalMs?: number, retryTimeoutMs?: number } = {}) {
const {
pollingIntervalMs = 1000,
retryTimeoutMs = 60000,
} = options
log(`Got receipt, filtering UserRequestForSignature from ${tr.events!.length} events...`)
// event UserRequestForSignature(bytes32 indexed messageId, bytes encodedData);
const sigEventArgsArray = tr.events!.filter((e: Todo) => e.event === 'UserRequestForSignature').map((e: Todo) => e.args)
if (sigEventArgsArray.length < 1) {
throw new Error("No UserRequestForSignature events emitted from withdraw transaction, can't transport withdraw to mainnet")
}

/* eslint-disable no-await-in-loop */
// eslint-disable-next-line no-restricted-syntax
for (const eventArgs of sigEventArgsArray) {
const messageId = eventArgs[0]
const messageHash = keccak256(eventArgs[1])

log(`Waiting until sidechain AMB has collected required signatures for hash=${messageHash}...`)
await until(async () => this.requiredSignaturesHaveBeenCollected(messageHash), pollingIntervalMs, retryTimeoutMs)

log(`Checking mainnet AMB hasn't already processed messageId=${messageId}`)
const mainnetAmb = await this.getMainnetAmb()
const alreadySent = await mainnetAmb.messageCallStatus(messageId)
const failAddress = await mainnetAmb.failedMessageSender(messageId)

// zero address means no failed messages
if (alreadySent || failAddress !== '0x0000000000000000000000000000000000000000') {
log(`WARNING: Mainnet bridge has already processed withdraw messageId=${messageId}`)
log('This could happen if freeWithdraw=false (attempt self-service), but bridge actually paid before your client')
continue
}

log(`Transporting signatures for hash=${messageHash}`)
await this.transportSignaturesForMessage(messageHash)
}
/* eslint-enable no-await-in-loop */
}

async deployDataUnion({
ownerAddress,
agentAddressList,
Expand Down
Loading