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

client/asset/eth: add RPC client #1832

Merged
merged 17 commits into from
Oct 19, 2022
Merged

client/asset/eth: add RPC client #1832

merged 17 commits into from
Oct 19, 2022

Conversation

buck54321
Copy link
Member

client/asset/eth:
Adds an RPC client that can maintain one or more websocket (preferred), http, or
ipc connections. Implements ethFetcher as well as bind.ContractBackend. Websocket
connections have a block header feed, so need to make fewer requests. For http
connections, the block header requests are metered to one per 10 seconds.
If no nonces are involved, the provider to use for a request is picked randomly,
but closely grouped requests involving nonces will attempt to use the same provider.

client/asset: Add Repeatable bool field to ConfigOption to allow multiple
inputs.

ui: Work with new repeatable input type.

Copy link
Contributor

@martonp martonp left a comment

Choose a reason for hiding this comment

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

Looks really nice, but I was unable to get it to work on simnet. I used the ipc file to add a provider, and at first it says it has connected to a peer, but when I try to make a trade, it fails saying that it has no peers.

Also, there is a UI issue. If I have a native wallet already created, I cannot see the Recieve/Send buttons, and I cannot switch to the rpc wallet. When I have no wallets created, I do not see the option of creating a native wallet.

client/asset/eth/eth.go Show resolved Hide resolved
}

m.receipts.Lock()
m.receipts.cache[txHash] = &receiptRecord{
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we shouldn't cache until the receipt has a certain number of confirmations. If we cache in the first block it is mined, then there is a reorg, it could cause issues.

lastProvider = p
m.log.Tracef("Sending signed tx via %q", p.host)
err := p.ec.SendTransaction(ctx, tx)
if err != nil && strings.Contains(err.Error(), core.ErrAlreadyKnown.Error()) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Why not errors.Is instead of the strings.Contains?

Copy link
Member Author

Choose a reason for hiding this comment

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

Coming through the (rpc.Client).CallContext, we lose the type.

}

func (m *multiRPCClient) syncProgress(ctx context.Context) (prog *ethereum.SyncProgress, err error) {
return prog, m.withAny(func(p *provider) error {
Copy link
Contributor

Choose a reason for hiding this comment

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

What if some providers are synced and some are not? Should we just return 100% synced progress, and in the other methds use the providers that have already finished syncing?

client/asset/eth/multirpc.go Show resolved Hide resolved
@@ -21,6 +21,13 @@
<input type="date" class="form-control select">
</div>
</div>
<div data-tmpl="repeatableInput" class="w-100">
Copy link
Contributor

Choose a reason for hiding this comment

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

This should have an option to remove additional rows.

@buck54321
Copy link
Member Author

Looks really nice, but I was unable to get it to work on simnet. I used the ipc file to add a provider, and at first it says it has connected to a peer, but when I try to make a trade, it fails saying that it has no peers.

I think I've resolved some things now @martonp, if you're up for another spin.

Also, there is a UI issue. If I have a native wallet already created, I cannot see the Recieve/Send buttons, and I cannot switch to the rpc wallet. When I have no wallets created, I do not see the option of creating a native wallet.

Oh. Interesting. Cuz I've removed support for native wallets. I'll figure this out.

Copy link
Contributor

@martonp martonp left a comment

Choose a reason for hiding this comment

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

All the functionality seems to be working well now, except for the redemption confirmation which works after fixing getTransaction.

return err
}
tx = resp.tx
if resp.BlockNumber != nil {
Copy link
Contributor

Choose a reason for hiding this comment

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

This resp.BlockNumber string has a "0x" in the front, causing the hex.DecodeString call to return an error. Also this function should return -1 for the height if the tx has not yet been mined.

Copy link
Member

@JoeGruffins JoeGruffins left a comment

Choose a reason for hiding this comment

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

partial review

Comment on lines 532 to 537
providers, err := connectProviders(ctx, endpoints, createWalletParams.Logger, big.NewInt(chainIDs[createWalletParams.Net]))
if err != nil {
return err
}
Copy link
Member

Choose a reason for hiding this comment

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

Feels weird to have to connect in create, but I see it runs some initial checks. Still maybe could be done in the first Connect. We have a ctx arg there and not here intentionally.

// use the same provider if < nonceProviderStickiness has passed.
var nonceProviderStickiness = time.Minute

// TODO: Handle rate limiting? From the docs:
Copy link
Member

Choose a reason for hiding this comment

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

Which docs?

ws bool
tipCapV atomic.Value // *cachedTipCap

// tip tracks the best known header as well as any error encount
Copy link
Member

Choose a reason for hiding this comment

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

encount -> encountered

Comment on lines +103 to +111
stale := time.Second * 10
if p.ws {
stale = time.Minute * 2
}
Copy link
Member

@JoeGruffins JoeGruffins Sep 12, 2022

Choose a reason for hiding this comment

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

Why longer for websocket? (is ws websocket)

Oh, because subscription maybe?

Copy link
Member Author

Choose a reason for hiding this comment

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

Right. We expect them to come through the subscription.

Comment on lines +125 to +131
// failed will be true if setFailed has been called in the last failQuarantine.
func (p *provider) failed() bool {
Copy link
Member

Choose a reason for hiding this comment

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

Also true if the provider has failed > 100 times.

Comment on lines +659 to +712
if strings.Contains(errStr, s) {
return true
}
Copy link
Member

Choose a reason for hiding this comment

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

Probably fine, but if mi is not a string or error s will always be "" and have a true return.

Copy link
Member Author

Choose a reason for hiding this comment

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

That's true, and I think I'm okay with it. I would understand if anyone was adamant that I do it different though.

}
if len(readyProviders) == 0 {
// Just try them all.
m.log.Tracef("all providers in a failed state, so acting like none are")
Copy link
Member

Choose a reason for hiding this comment

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

Maybe this is a warn?

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 this would be somewhat common when using a single provider.

Comment on lines 685 to 740
if superError == nil {
superError = err
} else {
superError = fmt.Errorf("%v: %w", superError, err)
}
Copy link
Member

@JoeGruffins JoeGruffins Sep 12, 2022

Choose a reason for hiding this comment

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

Not sure if this maybe returns below before the error log, but could add info about the provider to this error?

It seems like the error is just ignored sometimes. Could always log anyway? Debug log even if it's not really a problem?


for _, p := range m.providers {
if lastProvider != nil && lastProvider.host == p.host {
continue // already added it
Copy link
Member

Choose a reason for hiding this comment

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

Is comment correct? Looks like will add later. Or rather add everything else to it.

Comment on lines 777 to 779
// This block choosing algo is probably too rudimentary. Really need
// shnuld traverse parents to a common block and sum up gas (including
// uncles?), I think.
Copy link
Member

Choose a reason for hiding this comment

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

shnuld -> should

Why is GasUsed is in the equation?

We don't need to worry about uncles as they will not exist anymore, according to reddit https://www.reddit.com/r/ethstaker/comments/vwdki8/will_ommeruncle_blocks_exist_after_the_merge/

Copy link
Member Author

Choose a reason for hiding this comment

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

Isn't GasUsed the rough equivalent of work in ethereum? I'm not clear on how that works I guess.

Copy link
Member

Choose a reason for hiding this comment

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

I'm not either. It may be that more gas used has more weight.

Copy link
Member

Choose a reason for hiding this comment

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

I wouldn't guess that gas used has any thing to do with it either. Not saying it doesn't, but are there any references on this?

@chappjc chappjc self-requested a review September 12, 2022 12:39
return nil
}
if propagate {
return err
Copy link
Member

Choose a reason for hiding this comment

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

Maybe intended but this eats superError

Comment on lines +698 to +751
if fail {
p.setFailed()
}
Copy link
Member

@JoeGruffins JoeGruffins Sep 13, 2022

Choose a reason for hiding this comment

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

May be misreading, but if there were two providers and the first one errored and took this fail path while the second one succeeded then method would return the non nil superError although succeeding with the second provider.

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 had that messed up. Thanks.

// depending on what they are. Zero or more acceptabilityFilters can be added
// to provide extra control.
//
// discard: If a filter indicates discard = true, the error will be discarded,
Copy link
Member

Choose a reason for hiding this comment

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

nit: extra space in front of discard.

Comment on lines +836 to +911
func (m *multiRPCClient) sendSignedTransaction(ctx context.Context, tx *types.Transaction) error {
var lastProvider *provider
if err := m.withPreferred(func(p *provider) error {
lastProvider = p
m.log.Tracef("Sending signed tx via %q", p.host)
return p.ec.SendTransaction(ctx, tx)
}, func(err error) (discard, propagate, fail bool) {
return errorFilter(err, core.ErrAlreadyKnown, "known transaction"), false, false
}); err != nil {
return err
}
m.lastProvider.Lock()
m.lastProvider.provider = lastProvider
m.lastProvider.stamp = time.Now()
m.lastProvider.Unlock()
return nil
}
Copy link
Member

Choose a reason for hiding this comment

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

It looks like two of these running in parallel could have different providers set in close proximity, possible defeating the purpose of keeping the nonce in order.

Comment on lines +877 to +937
// syncProgress: We're going to lie and just always say we're synced if we
// can get a header.
Copy link
Member

Choose a reason for hiding this comment

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

Could make an exception for IPC?

Comment on lines 901 to 962
if strings.Contains(err.Error(), "not found") {
notFound = true
}
Copy link
Member

Choose a reason for hiding this comment

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

Could use the errorFilter you made.

Copy link
Member Author

@buck54321 buck54321 Sep 18, 2022

Choose a reason for hiding this comment

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

Can't use it here, because a subsequent provider might find it.

Comment on lines +1085 to +1158
type rpcTest struct {
name string
f func(*provider) error
}
Copy link
Member

Choose a reason for hiding this comment

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

Testing things could be in a test file?

@JoeGruffins
Copy link
Member

Maybe unrelated, but seeing for the first time on loadbot:

2022-09-13 15:54:52.160 [INF] MANTLE:CMPD:STACKER:1: created wallet dcr:iJt1 on node alpha
2022-09-13 15:54:52.189 [ERR] LOADBOT: exec error (dcr) "./alpha": exit status 1: -4: wallet.SendOutputs: double spend:: wallet.processTransactionRecord: 2f3cce875cae0b95a5d9b4deb30fc578f64ca2d4183eea84b0b8271f3dc66572 conflicts with 8fe8d127a39ecccf0d902fb4cabfbb32b8913948f801aacf64e71d4a9c1ba179 by double spending 6d58f2b8f0fb8412a4405dd9a99938733cec08256593c9992ca9d1199a1a54c6:2

2022-09-13 15:54:52.189 [CRT] MANTLE:CMPD:SNIPER:0: exit status 1
2022-09-13 15:54:52.189 [ERR] LOADBOT: exec error (dcr) "./alpha": exit status 1: -4: wallet.SendOutputs: double spend:: wallet.processTransactionRecord: 2dd5e335aac0763164798611d737e2bfdcd6bbc23b614b368306e7d1cd1c305d conflicts with 8fe8d127a39ecccf0d902fb4cabfbb32b8913948f801aacf64e71d4a9c1ba179 by double spending ecbe09281f2ae1d0f1144c07c518b156941cc6e437b5badadbfae9b6788840fa:0

2022-09-13 15:54:52.189 [CRT] MANTLE:CMPD:PINGPONG:0: exit status 1

Seems to have been random as has not happened again.

@JoeGruffins
Copy link
Member

JoeGruffins commented Sep 13, 2022

Trying to set up with the alpha rpc, I guess home symbol is not expanded here and I get the error 2022-09-13 16:23:48.491 [ERR] CORE[CREATE]: error creating http client for "~/dextest/eth/alpha/node/geth.ipc": dial unix ~/dextest/eth/alpha/node/geth.ipc: connect: no such file or directory which is fine but it won't let me try again until I restart the app or wait for a panic that seems to come:
image

I can make infinite providers but cannot remove the boxes. Doesn't need to be fixed now I guess:
image

Working well using the simnet harness alpha node! My testnet has almost synced... probably. Will try to set up for testing there.

@JoeGruffins
Copy link
Member

JoeGruffins commented Sep 16, 2022

Loading up testnet, I am unable to change my wallet settings for eth, there are no options:

image

Comment on lines 2319 to 2358
if err != nil {
if errors.Is(err, asset.ErrWalletTypeDisabled) {
subject, details := c.formatDetails(TopicWalletTypeDeprecated, unbip(assetID))
c.notify(newWalletConfigNote(TopicWalletTypeDeprecated, subject, details, db.WarningLevel, nil))
}
return nil, fmt.Errorf("error opening wallet: %w", err)
}
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 started into a scheme where we just disable the wallet, but it was a can of worms. Instead, we're refusing to open a deprecated wallet in e.g. eth.NewWallet and issuing a warning. This is no problem for us right now, because the only other option for Ethereum is also a seeded wallet and will derive the same key. But if there was a need to export a seed from a deprecated wallet, we would need to get more sophisticated here.

@JoeGruffins
Copy link
Member

JoeGruffins commented Sep 21, 2022

panic while testing on testnet using infura:

panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x8 pc=0xbd4e81]

goroutine 5948022 [running]:
decred.org/dcrdex/client/asset/eth.(*multiRPCClient).transactionReceipt(0xc0002462c0, {0x1f0c358, 0xc001212200}, {0xe0, 0x40, 0x25, 0xcc, 0xc7, 0xe5, 0xc7, ...})
        /home/joe/git/dcrdex/client/asset/eth/multirpc.go:536 +0x1c1
decred.org/dcrdex/client/asset/eth.(*assetWallet).confirmRedemption(0xc000246210, {0xc01124eae0?, 0x4110c7?, 0x20?}, 0xc011c40300, 0xc000dfe501?)
        /home/joe/git/dcrdex/client/asset/eth/eth.go:3237 +0xd8a
decred.org/dcrdex/client/asset/eth.(*ETHWallet).ConfirmRedemption(0x1f03d40?, {0xc01124eae0?, 0xc000dfe570?, 0x1?}, 0xc004954c90?)
        /home/joe/git/dcrdex/client/asset/eth/eth.go:3003 +0x25
decred.org/dcrdex/client/core.(*trackedTrade).confirmRedemption(0xc0006b6300, 0xc001031a00)
        /home/joe/git/dcrdex/client/core/trade.go:2538 +0x165
decred.org/dcrdex/client/core.(*Core).tick(0xc00016d680, 0xc0006b6300)
        /home/joe/git/dcrdex/client/core/trade.go:1777 +0x1c7d
decred.org/dcrdex/client/core.(*Core).tickAsset.func1()
        /home/joe/git/dcrdex/client/core/core.go:963 +0x45
created by decred.org/dcrdex/client/core.(*Core).tickAsset
        /home/joe/git/dcrdex/client/core/core.go:962 +0x277

Unable to recover, keeps panicking when I start up.

Other than that working well!

@JoeGruffins
Copy link
Member

JoeGruffins commented Sep 21, 2022

Also, weird critical error for the server randomly @chappjc:

2022-09-21 14:22:56.000 [CRT] MKT: Archivist failing. Last unexpected error: pq: column "preimage" is of type bytea but expression is of type integer
2022-09-21 14:22:56.000 [CRT] MKT: aborting epoch processing on account of failing DB: pq: column "preimage" is of type bytea but expression is of type integer
2022-09-21 14:22:56.000 [DBG] MKT: epoch pump drained for market eth_dcr
2022-09-21 14:22:56.000 [INF] MKT: Market "eth_dcr" stopped.
2022-09-21 14:22:56.000 [INF] MKT: Market "eth_dcr" suspended after epoch 103983611, persist book = true.

full logs:
dcrdextestnet.txt

@buck54321
Copy link
Member Author

buck54321 commented Sep 21, 2022

panic while testing on testnet using infura:

panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x8 pc=0xbd4e81]

goroutine 5948022 [running]:
decred.org/dcrdex/client/asset/eth.(*multiRPCClient).transactionReceipt(0xc0002462c0, {0x1f0c358, 0xc001212200}, {0xe0, 0x40, 0x25, 0xcc, 0xc7, 0xe5, 0xc7, ...})
        /home/joe/git/dcrdex/client/asset/eth/multirpc.go:536 +0x1c1
decred.org/dcrdex/client/asset/eth.(*assetWallet).confirmRedemption(0xc000246210, {0xc01124eae0?, 0x4110c7?, 0x20?}, 0xc011c40300, 0xc000dfe501?)
        /home/joe/git/dcrdex/client/asset/eth/eth.go:3237 +0xd8a
decred.org/dcrdex/client/asset/eth.(*ETHWallet).ConfirmRedemption(0x1f03d40?, {0xc01124eae0?, 0xc000dfe570?, 0x1?}, 0xc004954c90?)
        /home/joe/git/dcrdex/client/asset/eth/eth.go:3003 +0x25
decred.org/dcrdex/client/core.(*trackedTrade).confirmRedemption(0xc0006b6300, 0xc001031a00)
        /home/joe/git/dcrdex/client/core/trade.go:2538 +0x165
decred.org/dcrdex/client/core.(*Core).tick(0xc00016d680, 0xc0006b6300)
        /home/joe/git/dcrdex/client/core/trade.go:1777 +0x1c7d
decred.org/dcrdex/client/core.(*Core).tickAsset.func1()
        /home/joe/git/dcrdex/client/core/core.go:963 +0x45
created by decred.org/dcrdex/client/core.(*Core).tickAsset
        /home/joe/git/dcrdex/client/core/core.go:962 +0x277

Unable to recover, keeps panicking when I start up.

Other than that working well!

My bad. Fixed, and I've actually tested it on testnet this time. I've got some orders up on testnet dcr-eth tonight if you get back around to testing.

@buck54321
Copy link
Member Author

Also, weird critical error for the server randomly @chappjc:

2022-09-21 14:22:56.000 [CRT] MKT: Archivist failing. Last unexpected error: pq: column "preimage" is of type bytea but expression is of type integer
2022-09-21 14:22:56.000 [CRT] MKT: aborting epoch processing on account of failing DB: pq: column "preimage" is of type bytea but expression is of type integer
2022-09-21 14:22:56.000 [DBG] MKT: epoch pump drained for market eth_dcr
2022-09-21 14:22:56.000 [INF] MKT: Market "eth_dcr" stopped.
2022-09-21 14:22:56.000 [INF] MKT: Market "eth_dcr" suspended after epoch 103983611, persist book = true.

full logs: dcrdextestnet.txt

Uh. This is kinda nuts.

@JoeGruffins
Copy link
Member

Trades worked!
Unrelated to this pr, but is this right? I try to market buy with 2 eth but that becomes .2 eth:
image
image

Comment on lines 233 to 261
case err, ok := <-sub.Err():
if !ok {
// Subscription cancelled
return
}
log.Errorf("%q header subscription error: %v", p.host, err)
log.Info("Attempting to resubscribe to %q block headers", p.host)
sub, err = newSub()
if err != nil { // context cancelled
return
}
Copy link
Member

Choose a reason for hiding this comment

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

Trying the nodeclient tests on testnet I will see:

2022-09-23 15:11:34.532 [ERR] ETHTEST: "goerli.infura.io" header subscription error: <nil>
2022-09-23 15:11:34.532 [WRN] ETHTEST: can't resubscribe to "goerli.infura.io" headers: client is closed
2022-09-23 15:11:36.135 [ERR] ETHTEST: "goerli.infura.io" header subscription error: <nil>
2022-09-23 15:11:36.135 [WRN] ETHTEST: can't resubscribe to "goerli.infura.io" headers: client is closed

Expected?

Copy link
Member

@chappjc chappjc left a comment

Choose a reason for hiding this comment

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

Initial comments. More today.

client/asset/eth/multirpc.go Show resolved Hide resolved
client/asset/eth/multirpc.go Outdated Show resolved Hide resolved
client/asset/eth/multirpc.go Show resolved Hide resolved
client/asset/eth/multirpc.go Show resolved Hide resolved
client/asset/eth/multirpc.go Outdated Show resolved Hide resolved
client/asset/eth/multirpc.go Outdated Show resolved Hide resolved
client/asset/eth/multirpc.go Outdated Show resolved Hide resolved
client/asset/eth/multirpc.go Outdated Show resolved Hide resolved
client/asset/eth/multirpc.go Show resolved Hide resolved
ec.Close()
log.Errorf("Failed to get header from %q: %v", endpoint, err)
continue
}
Copy link
Member

Choose a reason for hiding this comment

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

Maybe a ec.HeaderByHash(ctx, hdr.Hash()) check as well now that it's needed?

Comment on lines 609 to 614
// TODO
// TODO: Plug into the monitoredTx system from #1638.
// TODO
if tx, _, err = m.getTransaction(ctx, txHash); err != nil {
return nil, nil, err
}
Copy link
Member

Choose a reason for hiding this comment

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

Was this addition intentional? We already have the tx.

client/asset/eth/multirpc.go Show resolved Hide resolved
client/asset/eth/multirpc.go Show resolved Hide resolved
client/asset/eth/multirpc.go Outdated Show resolved Hide resolved
@chappjc
Copy link
Member

chappjc commented Oct 17, 2022

If everybody could circle back on their reviews and resolve or bump suggestions, we're nearly ready to get this in.
@martonp @JoeGruffins

client/asset/eth/eth.go Outdated Show resolved Hide resolved
client/asset/eth/eth.go Outdated Show resolved Hide resolved
client/asset/eth/eth.go Outdated Show resolved Hide resolved
client/asset/eth/multirpc.go Outdated Show resolved Hide resolved
client/asset/eth/multirpc.go Outdated Show resolved Hide resolved
client/asset/eth/multirpc.go Outdated Show resolved Hide resolved
@martonp
Copy link
Contributor

martonp commented Oct 17, 2022

https://github.com/decred/dcrdex/pull/1832/files#r965796162

@buck54321 You fixed this in 325a552 but somehow it is back now.

client/asset/eth:
Adds an RPC client that can maintain one or more websocket (preferred), http, or
ipc connections. Implements ethFetcher as well as bind.ContractBackend. Websocket
connections have a block header feed, so need to make fewer requests. For http
connections, the block header requests are metered to one per 10 seconds.
If no nonces are involved, the provider to use for a request is picked randomly,
but closely grouped requests involving nonces will attempt to use the same provider.

client/asset: Add Repeatable bool field to ConfigOption to allow multiple
inputs.

ui: Work with new repeatable input type.
@chappjc
Copy link
Member

chappjc commented Oct 19, 2022

OK on your reviews, @JoeGruffins?

@chappjc chappjc merged commit 921f7f6 into decred:master Oct 19, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants