diff --git a/.github/workflows/ark.unit.yaml b/.github/workflows/ark.unit.yaml index 5f6e46f..804ce6c 100755 --- a/.github/workflows/ark.unit.yaml +++ b/.github/workflows/ark.unit.yaml @@ -47,14 +47,15 @@ jobs: go-version: '>=1.22.6' - uses: actions/checkout@v3 - name: check linting - uses: golangci/golangci-lint-action@v3 + uses: golangci/golangci-lint-action@v6 with: - version: v1.54 + version: v1.61 working-directory: ./server + args: --timeout 5m - name: check code integrity uses: securego/gosec@master with: - args: "-severity high -quiet ./..." + args: "-severity high -quiet -exclude=G115 ./..." - run: go get -v -t -d ./... - name: unit testing run: make test @@ -71,14 +72,15 @@ jobs: go-version: '>=1.22.6' - uses: actions/checkout@v3 - name: check linting - uses: golangci/golangci-lint-action@v3 + uses: golangci/golangci-lint-action@v6 with: - version: v1.54 + version: v1.61 working-directory: ./pkg/client-sdk + args: --timeout 5m - name: check code integrity uses: securego/gosec@master with: - args: "-severity high -quiet ./..." + args: "-severity high -quiet -exclude=G115 ./..." - run: go get -v -t -d ./... - name: unit testing run: make test diff --git a/Dockerfile b/Dockerfile index 28a6f0f..7b3c79b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # First image used to build the sources -FROM golang:1.21.0 AS builder +FROM golang:1.23.1 AS builder ARG VERSION ARG TARGETOS @@ -14,7 +14,7 @@ RUN cd server && CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -l RUN cd client && CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -ldflags="-X 'main.Version=${VERSION}'" -o ../bin/ark . # Second image, running the arkd executable -FROM alpine:3.18 +FROM alpine:3.20 RUN apk update && apk upgrade diff --git a/buf.Dockerfile b/buf.Dockerfile index 4589801..7a20828 100644 --- a/buf.Dockerfile +++ b/buf.Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.21-alpine3.18 as builder +FROM golang:1.23.1-alpine3.20 as builder RUN apk add --no-cache git diff --git a/client/go.mod b/client/go.mod index 848bcac..0f77791 100644 --- a/client/go.mod +++ b/client/go.mod @@ -1,6 +1,6 @@ module github.com/ark-network/ark/client -go 1.22.6 +go 1.23.1 replace github.com/ark-network/ark/common => ../common diff --git a/client/utils/cypher.go b/client/utils/cypher.go index 6b298d2..7583214 100644 --- a/client/utils/cypher.go +++ b/client/utils/cypher.go @@ -80,6 +80,7 @@ func (c *cypher) decrypt(encrypted, password []byte) ([]byte, error) { return nil, err } nonce, text := data[:gcm.NonceSize()], data[gcm.NonceSize():] + // #nosec G407 plaintext, err := gcm.Open(nil, nonce, text, nil) if err != nil { return nil, fmt.Errorf("invalid password") diff --git a/common/go.mod b/common/go.mod index 4b207a0..d76e35f 100644 --- a/common/go.mod +++ b/common/go.mod @@ -1,6 +1,6 @@ module github.com/ark-network/ark/common -go 1.22.6 +go 1.23.1 replace github.com/btcsuite/btcd/btcec/v2 => github.com/btcsuite/btcd/btcec/v2 v2.3.3 diff --git a/go.work b/go.work index 61bd57f..3e94142 100644 --- a/go.work +++ b/go.work @@ -1,4 +1,4 @@ -go 1.22.6 +go 1.23.1 use ( ./api-spec diff --git a/pkg/client-sdk/ark_sdk.go b/pkg/client-sdk/ark_sdk.go index 57540a5..c8f87d7 100644 --- a/pkg/client-sdk/ark_sdk.go +++ b/pkg/client-sdk/ark_sdk.go @@ -27,6 +27,7 @@ type ArkClient interface { SendAsync(ctx context.Context, withExpiryCoinselect bool, receivers []Receiver) (string, error) Claim(ctx context.Context) (string, error) ListVtxos(ctx context.Context) ([]client.Vtxo, []client.Vtxo, error) + GetTransactionHistory(ctx context.Context) ([]Transaction, error) } type Receiver interface { diff --git a/pkg/client-sdk/client.go b/pkg/client-sdk/client.go index 991b2a0..e545a4c 100644 --- a/pkg/client-sdk/client.go +++ b/pkg/client-sdk/client.go @@ -303,3 +303,7 @@ func getWalletStore(storeType, datadir string) (walletstore.WalletStore, error) return nil, fmt.Errorf("unknown wallet store type") } } + +func getCreatedAtFromExpiry(roundLifetime int64, expiry time.Time) time.Time { + return expiry.Add(-time.Duration(roundLifetime) * time.Second) +} diff --git a/pkg/client-sdk/client_test.go b/pkg/client-sdk/client_test.go new file mode 100644 index 0000000..9f6a696 --- /dev/null +++ b/pkg/client-sdk/client_test.go @@ -0,0 +1,567 @@ +package arksdk + +import ( + "encoding/json" + "fmt" + "strconv" + "testing" + "time" + + "github.com/ark-network/ark/pkg/client-sdk/client" + "github.com/stretchr/testify/require" +) + +func TestVtxosToTxs(t *testing.T) { + tests := []struct { + name string + fixture string + want []Transaction + }{ + { + name: "Alice Before Sending Async", + fixture: aliceBeforeSendingAsync, + want: []Transaction{ + { + RoundTxid: "377fa2fbd27c82bdbc095478384c88b6c75432c0ef464189e49c965194446cdf", + Amount: 20000, + Type: TxReceived, + Pending: false, + Claimed: true, + CreatedAt: time.Unix(1726054898, 0), + }, + }, + }, + { + name: "Alice After Sending Async", + fixture: aliceAfterSendingAsync, + want: []Transaction{ + { + RedeemTxid: "94fa598302f17f00c8881e742ec0ce2f8c8d16f3d54fe6ba0fb7d13a493d84ad", + Amount: 1000, + Type: TxSent, + Pending: true, + Claimed: false, + CreatedAt: time.Unix(1726054898, 0), + }, + { + RoundTxid: "377fa2fbd27c82bdbc095478384c88b6c75432c0ef464189e49c965194446cdf", + Amount: 20000, + Type: TxReceived, + Pending: false, + Claimed: true, + CreatedAt: time.Unix(1726054898, 0), + }, + }, + }, + { + name: "Bob Before Claiming Async", + fixture: bobBeforeClaimingAsync, + want: []Transaction{ + { + RedeemTxid: "766fc46ba5c2da41cd4c4bc0566e0f4e0f24c184c41acd3bead5cd7b11120367", + Amount: 2000, + Type: TxReceived, + Pending: true, + Claimed: false, + CreatedAt: time.Unix(1726486359, 0), + }, + { + RedeemTxid: "94fa598302f17f00c8881e742ec0ce2f8c8d16f3d54fe6ba0fb7d13a493d84ad", + Amount: 1000, + Type: TxReceived, + Pending: true, + Claimed: false, + CreatedAt: time.Unix(1726054898, 0), + }, + }, + }, + { + name: "Bob After Claiming Async", + fixture: bobAfterClaimingAsync, + want: []Transaction{ + { + RedeemTxid: "766fc46ba5c2da41cd4c4bc0566e0f4e0f24c184c41acd3bead5cd7b11120367", + Amount: 2000, + Type: TxReceived, + Pending: false, + Claimed: true, + CreatedAt: time.Unix(1726486359, 0), + }, + { + RedeemTxid: "94fa598302f17f00c8881e742ec0ce2f8c8d16f3d54fe6ba0fb7d13a493d84ad", + Amount: 1000, + Type: TxReceived, + Pending: false, + Claimed: true, + CreatedAt: time.Unix(1726054898, 0), + }, + }, + }, + { + name: "Bob After Sending Async", + fixture: bobAfterSendingAsync, + want: []Transaction{ + { + RedeemTxid: "23c3a885f0ea05f7bdf83f3bf7f8ac9dc3f791ad292f4e63a6f53fa5e4935ab0", + Amount: 2100, + Type: TxSent, + Pending: true, + Claimed: false, + CreatedAt: time.Unix(1726503865, 0), + }, + { + RedeemTxid: "766fc46ba5c2da41cd4c4bc0566e0f4e0f24c184c41acd3bead5cd7b11120367", + Amount: 2000, + Type: TxReceived, + Pending: false, + Claimed: true, + CreatedAt: time.Unix(1726486359, 0), + }, + { + RedeemTxid: "94fa598302f17f00c8881e742ec0ce2f8c8d16f3d54fe6ba0fb7d13a493d84ad", + Amount: 1000, + Type: TxReceived, + Pending: false, + Claimed: true, + CreatedAt: time.Unix(1726054898, 0), + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + args, err := loadFixtures(tt.fixture) + if err != nil { + t.Fatalf("failed to load fixture: %s", err) + } + got, err := vtxosToTxsCovenantless(30, args.spendable, args.spent) + require.NoError(t, err) + require.Len(t, got, len(tt.want)) + + // Check each expected transaction, excluding CreatedAt + for i, wantTx := range tt.want { + gotTx := got[i] + require.Equal(t, wantTx.RoundTxid, gotTx.RoundTxid) + require.Equal(t, wantTx.RedeemTxid, gotTx.RedeemTxid) + require.Equal(t, int(wantTx.Amount), int(gotTx.Amount)) + require.Equal(t, wantTx.Type, gotTx.Type) + require.Equal(t, wantTx.Pending, gotTx.Pending) + require.Equal(t, wantTx.Claimed, gotTx.Claimed) + } + }) + } +} + +type vtxos struct { + spendable []client.Vtxo + spent []client.Vtxo +} + +func loadFixtures(jsonStr string) (vtxos, error) { + var data struct { + SpendableVtxos []struct { + Outpoint struct { + Txid string `json:"txid"` + Vout uint32 `json:"vout"` + } `json:"outpoint"` + Receiver struct { + Address string `json:"address"` + Amount string `json:"amount"` + } `json:"receiver"` + Spent bool `json:"spent"` + PoolTxid string `json:"poolTxid"` + SpentBy string `json:"spentBy"` + ExpireAt string `json:"expireAt"` + Swept bool `json:"swept"` + Pending bool `json:"pending"` + PendingData struct { + RedeemTx string `json:"redeemTx"` + UnconditionalForfeitTxs []string `json:"unconditionalForfeitTxs"` + } `json:"pendingData"` + } `json:"spendableVtxos"` + SpentVtxos []struct { + Outpoint struct { + Txid string `json:"txid"` + Vout uint32 `json:"vout"` + } `json:"outpoint"` + Receiver struct { + Address string `json:"address"` + Amount string `json:"amount"` + } `json:"receiver"` + Spent bool `json:"spent"` + PoolTxid string `json:"poolTxid"` + SpentBy string `json:"spentBy"` + ExpireAt string `json:"expireAt"` + Swept bool `json:"swept"` + Pending bool `json:"pending"` + PendingData struct { + RedeemTx string `json:"redeemTx"` + UnconditionalForfeitTxs []string `json:"unconditionalForfeitTxs"` + } `json:"pendingData"` + } `json:"spentVtxos"` + } + + if err := json.Unmarshal([]byte(jsonStr), &data); err != nil { + return vtxos{}, err + } + + spendable := make([]client.Vtxo, len(data.SpendableVtxos)) + for i, vtxo := range data.SpendableVtxos { + expireAt, err := parseTimestamp(vtxo.ExpireAt) + if err != nil { + return vtxos{}, err + } + amount, err := parseAmount(vtxo.Receiver.Amount) + if err != nil { + return vtxos{}, err + } + spendable[i] = client.Vtxo{ + VtxoKey: client.VtxoKey{ + Txid: vtxo.Outpoint.Txid, + VOut: vtxo.Outpoint.Vout, + }, + Amount: amount, + RoundTxid: vtxo.PoolTxid, + ExpiresAt: &expireAt, + RedeemTx: vtxo.PendingData.RedeemTx, + UnconditionalForfeitTxs: vtxo.PendingData.UnconditionalForfeitTxs, + Pending: vtxo.Pending, + SpentBy: vtxo.SpentBy, + } + } + + spent := make([]client.Vtxo, len(data.SpentVtxos)) + for i, vtxo := range data.SpentVtxos { + expireAt, err := parseTimestamp(vtxo.ExpireAt) + if err != nil { + return vtxos{}, err + } + amount, err := parseAmount(vtxo.Receiver.Amount) + if err != nil { + return vtxos{}, err + } + spent[i] = client.Vtxo{ + VtxoKey: client.VtxoKey{ + Txid: vtxo.Outpoint.Txid, + VOut: vtxo.Outpoint.Vout, + }, + Amount: amount, + RoundTxid: vtxo.PoolTxid, + ExpiresAt: &expireAt, + RedeemTx: vtxo.PendingData.RedeemTx, + UnconditionalForfeitTxs: vtxo.PendingData.UnconditionalForfeitTxs, + Pending: vtxo.Pending, + SpentBy: vtxo.SpentBy, + } + } + + return vtxos{ + spendable: spendable, + spent: spent, + }, nil +} + +func parseAmount(amountStr string) (uint64, error) { + amount, err := strconv.ParseUint(amountStr, 10, 64) + if err != nil { + return 0, err + } + + return amount, nil +} + +func parseTimestamp(timestamp string) (time.Time, error) { + seconds, err := strconv.ParseInt(timestamp, 10, 64) + if err != nil { + return time.Time{}, fmt.Errorf("invalid timestamp format: %w", err) + } + + return time.Unix(seconds, 0), nil +} + +// bellow fixtures are used in bellow scenario: +// 1. Alice boards with 20OOO +// 2. Alice sends 1000 to Bob +// 3. Bob claims 1000 +var ( + aliceBeforeSendingAsync = ` + { + "spendableVtxos": [ + { + "outpoint": { + "txid": "69ccb6520e0b91ac1cbaa459b16ec1e3ff5f6349990b0d149dd8e6c6485d316c", + "vout": 0 + }, + "receiver": { + "address": "tark1qwnakvl59d5wckz9lqhhdav0uvns6uu3zkc6hg65gh0kgh6wve9pwqa0qjq9ajm57ss4m7wutyhp3vexxzgkn2r5awtzytp8qfk8exfn4vm5d8ff", + "amount": "20000" + }, + "spent": false, + "poolTxid": "377fa2fbd27c82bdbc095478384c88b6c75432c0ef464189e49c965194446cdf", + "spentBy": "", + "expireAt": "1726054928", + "swept": false, + "pending": false, + "pendingData": null + } + ], + "spentVtxos": [] + }` + + aliceAfterSendingAsync = ` + { + "spendableVtxos": [ + { + "outpoint": { + "txid": "94fa598302f17f00c8881e742ec0ce2f8c8d16f3d54fe6ba0fb7d13a493d84ad", + "vout": 1 + }, + "receiver": { + "address": "tark1qwnakvl59d5wckz9lqhhdav0uvns6uu3zkc6hg65gh0kgh6wve9pwqa0qjq9ajm57ss4m7wutyhp3vexxzgkn2r5awtzytp8qfk8exfn4vm5d8ff", + "amount": "19000" + }, + "spent": false, + "poolTxid": "", + "spentBy": "", + "expireAt": "1726054928", + "swept": false, + "pending": true, + "pendingData": { + "redeemTx": "cHNidP8BAIkCAAAAAWwxXUjG5tidFA0LmUljX//jwW6xWaS6HKyRCw5StsxpAAAAAAD/////AugDAAAAAAAAIlEgt2eR8LtqTP7yUcQtSydeGrRiHnVmHHnZwYjdC23G7MZwSQAAAAAAACJRIKfUzf/o9h+r0v9y4nmyOt9qO8EkDumQPQZGTbEv8fSFAAAAAAABASsgTgAAAAAAACJRIKfUzf/o9h+r0v9y4nmyOt9qO8EkDumQPQZGTbEv8fSFIgYDp9sz9Cto7FhF+C929Y/jJw1zkRWxq6NURd9kX05mShcYAAAAAFYAAIAAAACAAQAAgAAAAAAAAAAAQRSvBIBey3T0IV353FkuGLMmMJFpqHTrliIsJwJsfJkzq7J0B8bQ0j9842h5lUfOWcbj2TeoFx6OCpgoHIqWIBhHQAFqkBLiRmP3AZ8MS77s1QIWZswMV3L72D9gN0f0MbD6XHkmzZeC1clF3uzxr+13wsF0vcFe29Zl3e2gAhMNGYVCFcFQkpt0waBJVLeLS2A16XpeB4paDyjsltVHv+6azoA6wKRtST8P7teUpSF4DAEbfJj5OIXITx5QGbZns/AtxqGyRSCn2zP0K2jsWEX4L3b1j+MnDXORFbGro1RF32RfTmZKF60grwSAXst09CFd+dxZLhizJjCRaah065YiLCcCbHyZM6uswCEWp9sz9Cto7FhF+C929Y/jJw1zkRWxq6NURd9kX05mShc5AbJ0B8bQ0j9842h5lUfOWcbj2TeoFx6OCpgoHIqWIBhHAAAAAFYAAIAAAACAAQAAgAAAAAAAAAAAARcgUJKbdMGgSVS3i0tgNel6XgeKWg8o7JbVR7/ums6AOsAAAAA=", + "unconditionalForfeitTxs": [ + "cHNidP8BAFICAAAAAWwxXUjG5tidFA0LmUljX//jwW6xWaS6HKyRCw5StsxpAAAAAAD/////AVhNAAAAAAAAFgAUSU38/3Mzx5BdILG4oUO+JoHcoT8AAAAAAAEBKyBOAAAAAAAAIlEgp9TN/+j2H6vS/3LiebI632o7wSQO6ZA9BkZNsS/x9IVBFK8EgF7LdPQhXfncWS4YsyYwkWmodOuWIiwnAmx8mTOrsnQHxtDSP3zjaHmVR85ZxuPZN6gXHo4KmCgcipYgGEdAjH8Mg1Z3GdjGzp78Mg2xq1fop9KDfeji+xoyMgYS7q0Nl0AGOAaNzkDRW4cNcefll5jZC2i3nfygKdXsUsR+LEIVwVCSm3TBoElUt4tLYDXpel4HiloPKOyW1Ue/7prOgDrApG1JPw/u15SlIXgMARt8mPk4hchPHlAZtmez8C3GobJFIKfbM/QraOxYRfgvdvWP4ycNc5EVsaujVEXfZF9OZkoXrSCvBIBey3T0IV353FkuGLMmMJFpqHTrliIsJwJsfJkzq6zAARcgUJKbdMGgSVS3i0tgNel6XgeKWg8o7JbVR7/ums6AOsAAAA==" + ] + } + } + ], + "spentVtxos": [ + { + "outpoint": { + "txid": "69ccb6520e0b91ac1cbaa459b16ec1e3ff5f6349990b0d149dd8e6c6485d316c", + "vout": 0 + }, + "receiver": { + "address": "tark1qwnakvl59d5wckz9lqhhdav0uvns6uu3zkc6hg65gh0kgh6wve9pwqa0qjq9ajm57ss4m7wutyhp3vexxzgkn2r5awtzytp8qfk8exfn4vm5d8ff", + "amount": "20000" + }, + "spent": true, + "poolTxid": "377fa2fbd27c82bdbc095478384c88b6c75432c0ef464189e49c965194446cdf", + "spentBy": "94fa598302f17f00c8881e742ec0ce2f8c8d16f3d54fe6ba0fb7d13a493d84ad", + "expireAt": "1726054928", + "swept": false, + "pending": false, + "pendingData": null + } + ] + }` + + bobBeforeClaimingAsync = ` + { + "spendableVtxos": [ + { + "outpoint": { + "txid": "94fa598302f17f00c8881e742ec0ce2f8c8d16f3d54fe6ba0fb7d13a493d84ad", + "vout": 0 + }, + "receiver": { + "address": "tark1qwnakvl59d5wckz9lqhhdav0uvns6uu3zkc6hg65gh0kgh6wve9pwqa8vzms5xcr7pqgt0sw88vc287dse5rw6fnxuk9f08frf8amxjcrya0tkgt", + "amount": "1000" + }, + "spent": false, + "poolTxid": "", + "spentBy": "", + "expireAt": "1726054928", + "swept": false, + "pending": true, + "pendingData": { + "redeemTx": "cHNidP8BAIkCAAAAAWwxXUjG5tidFA0LmUljX//jwW6xWaS6HKyRCw5StsxpAAAAAAD/////AugDAAAAAAAAIlEgt2eR8LtqTP7yUcQtSydeGrRiHnVmHHnZwYjdC23G7MZwSQAAAAAAACJRIKfUzf/o9h+r0v9y4nmyOt9qO8EkDumQPQZGTbEv8fSFAAAAAAABASsgTgAAAAAAACJRIKfUzf/o9h+r0v9y4nmyOt9qO8EkDumQPQZGTbEv8fSFIgYDp9sz9Cto7FhF+C929Y/jJw1zkRWxq6NURd9kX05mShcYAAAAAFYAAIAAAACAAQAAgAAAAAAAAAAAQRSvBIBey3T0IV353FkuGLMmMJFpqHTrliIsJwJsfJkzq7J0B8bQ0j9842h5lUfOWcbj2TeoFx6OCpgoHIqWIBhHQAFqkBLiRmP3AZ8MS77s1QIWZswMV3L72D9gN0f0MbD6XHkmzZeC1clF3uzxr+13wsF0vcFe29Zl3e2gAhMNGYVCFcFQkpt0waBJVLeLS2A16XpeB4paDyjsltVHv+6azoA6wKRtST8P7teUpSF4DAEbfJj5OIXITx5QGbZns/AtxqGyRSCn2zP0K2jsWEX4L3b1j+MnDXORFbGro1RF32RfTmZKF60grwSAXst09CFd+dxZLhizJjCRaah065YiLCcCbHyZM6uswCEWp9sz9Cto7FhF+C929Y/jJw1zkRWxq6NURd9kX05mShc5AbJ0B8bQ0j9842h5lUfOWcbj2TeoFx6OCpgoHIqWIBhHAAAAAFYAAIAAAACAAQAAgAAAAAAAAAAAARcgUJKbdMGgSVS3i0tgNel6XgeKWg8o7JbVR7/ums6AOsAAAAA=", + "unconditionalForfeitTxs": [ + "cHNidP8BAFICAAAAAWwxXUjG5tidFA0LmUljX//jwW6xWaS6HKyRCw5StsxpAAAAAAD/////AVhNAAAAAAAAFgAUSU38/3Mzx5BdILG4oUO+JoHcoT8AAAAAAAEBKyBOAAAAAAAAIlEgp9TN/+j2H6vS/3LiebI632o7wSQO6ZA9BkZNsS/x9IVBFK8EgF7LdPQhXfncWS4YsyYwkWmodOuWIiwnAmx8mTOrsnQHxtDSP3zjaHmVR85ZxuPZN6gXHo4KmCgcipYgGEdAjH8Mg1Z3GdjGzp78Mg2xq1fop9KDfeji+xoyMgYS7q0Nl0AGOAaNzkDRW4cNcefll5jZC2i3nfygKdXsUsR+LEIVwVCSm3TBoElUt4tLYDXpel4HiloPKOyW1Ue/7prOgDrApG1JPw/u15SlIXgMARt8mPk4hchPHlAZtmez8C3GobJFIKfbM/QraOxYRfgvdvWP4ycNc5EVsaujVEXfZF9OZkoXrSCvBIBey3T0IV353FkuGLMmMJFpqHTrliIsJwJsfJkzq6zAARcgUJKbdMGgSVS3i0tgNel6XgeKWg8o7JbVR7/ums6AOsAAAA==" + ] + } + }, + { + "outpoint": { + "txid": "766fc46ba5c2da41cd4c4bc0566e0f4e0f24c184c41acd3bead5cd7b11120367", + "vout": 0 + }, + "receiver": { + "address": "tark1qgw4gpt40zet7q399hv78z7pdak5sxlcgzhy6y6qq7hw3syeudc6xqsws4tegt5r88eahx7g5try2ua4n9rflsncpresjfwcrq80k0d3systnm98", + "amount": "2000" + }, + "spent": false, + "poolTxid": "", + "spentBy": "", + "expireAt": "1726486389", + "swept": false, + "pending": true, + "pendingData": { + "redeemTx": "cHNidP8BAIkCAAAAARH6LJRGP/pFIkD/o5bBp8fXAhjl8yjfN7MhJsxdt5lrAQAAAAD/////AtAHAAAAAAAAIlEguuBh3KQUVZp+NHV2sixQ/mrsngCuLCGXzsgJPC1FzY7ANQ8AAAAAACJRIP7uXXtl4jLUcVVQU+sX7WFXmx2H6iCMzn7gye0v1Y8JAAAAAAABAStYPg8AAAAAACJRIP7uXXtl4jLUcVVQU+sX7WFXmx2H6iCMzn7gye0v1Y8JIgYCHVQFdXiyvwIlLdnji8FvbUgb+ECuTRNAB67owJnjcaMYAAAAAFYAAIAAAACAAQAAgAAAAAAAAAAAQRSc1yjQ/vHRHev23fKGANLvbOhkNYmGtmRWt8fSszlOJUzRFbnfxd1fq9gIEpaI0vrZww8tlZ94iEL75QoaIbVqQJsPdLYf7fAoXO82VoqwYHu1WevE4g6LxUGBPzfd96q5EEZkoW5qqg+v5dWJUEY467Q6qZLFHwziUaB3KEY8yEpCFcBQkpt0waBJVLeLS2A16XpeB4paDyjsltVHv+6azoA6wO5D2Mh3x0XNGxFCS67GNughkENFodpFeVpZjn76chI8RSAdVAV1eLK/AiUt2eOLwW9tSBv4QK5NE0AHrujAmeNxo60gnNco0P7x0R3r9t3yhgDS72zoZDWJhrZkVrfH0rM5TiWswCEWHVQFdXiyvwIlLdnji8FvbUgb+ECuTRNAB67owJnjcaM5AUzRFbnfxd1fq9gIEpaI0vrZww8tlZ94iEL75QoaIbVqAAAAAFYAAIAAAACAAQAAgAAAAAAAAAAAARcgUJKbdMGgSVS3i0tgNel6XgeKWg8o7JbVR7/ums6AOsAAAAA=", + "unconditionalForfeitTxs": [ + "cHNidP8BAFICAAAAARH6LJRGP/pFIkD/o5bBp8fXAhjl8yjfN7MhJsxdt5lrAQAAAAD/////AZA9DwAAAAAAFgAU+9NJhjFhe8jX1hrXh3NvDyHZ1cYAAAAAAAEBK1g+DwAAAAAAIlEg/u5de2XiMtRxVVBT6xftYVebHYfqIIzOfuDJ7S/VjwlBFJzXKND+8dEd6/bd8oYA0u9s6GQ1iYa2ZFa3x9KzOU4lTNEVud/F3V+r2AgSlojS+tnDDy2Vn3iIQvvlChohtWpARJzBjlEkN/kTpyFEtpvP2Ui7ypevuxb9J/NUAwhYf8Pmnnj1l3WuKCSi4Fcp1O+lQjIiZlNpwY6J73q/V8Fe2kIVwFCSm3TBoElUt4tLYDXpel4HiloPKOyW1Ue/7prOgDrA7kPYyHfHRc0bEUJLrsY26CGQQ0Wh2kV5WlmOfvpyEjxFIB1UBXV4sr8CJS3Z44vBb21IG/hArk0TQAeu6MCZ43GjrSCc1yjQ/vHRHev23fKGANLvbOhkNYmGtmRWt8fSszlOJazAARcgUJKbdMGgSVS3i0tgNel6XgeKWg8o7JbVR7/ums6AOsAAAA==" + ] + } + } + ], + "spentVtxos": [] + }` + bobAfterClaimingAsync = ` + { + "spendableVtxos": [ + { + "outpoint": { + "txid": "11cba4cbb06290fb7426157efe439940e1e4143d51bdd20567d7bfd28f0d9090", + "vout": 0 + }, + "receiver": { + "address": "tark1qgw4gpt40zet7q399hv78z7pdak5sxlcgzhy6y6qq7hw3syeudc6xqsws4tegt5r88eahx7g5try2ua4n9rflsncpresjfwcrq80k0d3systnm98", + "amount": "3000" + }, + "spent": false, + "poolTxid": "d6684a5b9e6939dccdf07d1f0eaf7fdd7b31de4d123e63e400d23de739800d4e", + "spentBy": "", + "expireAt": "1726503895", + "swept": false, + "pending": false, + "pendingData": null + } + ], + "spentVtxos": [ + { + "outpoint": { + "txid": "94fa598302f17f00c8881e742ec0ce2f8c8d16f3d54fe6ba0fb7d13a493d84ad", + "vout": 0 + }, + "receiver": { + "address": "tark1qwnakvl59d5wckz9lqhhdav0uvns6uu3zkc6hg65gh0kgh6wve9pwqa8vzms5xcr7pqgt0sw88vc287dse5rw6fnxuk9f08frf8amxjcrya0tkgt", + "amount": "1000" + }, + "spent": true, + "poolTxid": "", + "spentBy": "d6684a5b9e6939dccdf07d1f0eaf7fdd7b31de4d123e63e400d23de739800d4e", + "expireAt": "1726054928", + "swept": false, + "pending": true, + "pendingData": { + "redeemTx": "cHNidP8BAIkCAAAAAWwxXUjG5tidFA0LmUljX//jwW6xWaS6HKyRCw5StsxpAAAAAAD/////AugDAAAAAAAAIlEgt2eR8LtqTP7yUcQtSydeGrRiHnVmHHnZwYjdC23G7MZwSQAAAAAAACJRIKfUzf/o9h+r0v9y4nmyOt9qO8EkDumQPQZGTbEv8fSFAAAAAAABASsgTgAAAAAAACJRIKfUzf/o9h+r0v9y4nmyOt9qO8EkDumQPQZGTbEv8fSFIgYDp9sz9Cto7FhF+C929Y/jJw1zkRWxq6NURd9kX05mShcYAAAAAFYAAIAAAACAAQAAgAAAAAAAAAAAQRSvBIBey3T0IV353FkuGLMmMJFpqHTrliIsJwJsfJkzq7J0B8bQ0j9842h5lUfOWcbj2TeoFx6OCpgoHIqWIBhHQAFqkBLiRmP3AZ8MS77s1QIWZswMV3L72D9gN0f0MbD6XHkmzZeC1clF3uzxr+13wsF0vcFe29Zl3e2gAhMNGYVCFcFQkpt0waBJVLeLS2A16XpeB4paDyjsltVHv+6azoA6wKRtST8P7teUpSF4DAEbfJj5OIXITx5QGbZns/AtxqGyRSCn2zP0K2jsWEX4L3b1j+MnDXORFbGro1RF32RfTmZKF60grwSAXst09CFd+dxZLhizJjCRaah065YiLCcCbHyZM6uswCEWp9sz9Cto7FhF+C929Y/jJw1zkRWxq6NURd9kX05mShc5AbJ0B8bQ0j9842h5lUfOWcbj2TeoFx6OCpgoHIqWIBhHAAAAAFYAAIAAAACAAQAAgAAAAAAAAAAAARcgUJKbdMGgSVS3i0tgNel6XgeKWg8o7JbVR7/ums6AOsAAAAA=", + "unconditionalForfeitTxs": [ + "cHNidP8BAFICAAAAAWwxXUjG5tidFA0LmUljX//jwW6xWaS6HKyRCw5StsxpAAAAAAD/////AVhNAAAAAAAAFgAUSU38/3Mzx5BdILG4oUO+JoHcoT8AAAAAAAEBKyBOAAAAAAAAIlEgp9TN/+j2H6vS/3LiebI632o7wSQO6ZA9BkZNsS/x9IVBFK8EgF7LdPQhXfncWS4YsyYwkWmodOuWIiwnAmx8mTOrsnQHxtDSP3zjaHmVR85ZxuPZN6gXHo4KmCgcipYgGEdAjH8Mg1Z3GdjGzp78Mg2xq1fop9KDfeji+xoyMgYS7q0Nl0AGOAaNzkDRW4cNcefll5jZC2i3nfygKdXsUsR+LEIVwVCSm3TBoElUt4tLYDXpel4HiloPKOyW1Ue/7prOgDrApG1JPw/u15SlIXgMARt8mPk4hchPHlAZtmez8C3GobJFIKfbM/QraOxYRfgvdvWP4ycNc5EVsaujVEXfZF9OZkoXrSCvBIBey3T0IV353FkuGLMmMJFpqHTrliIsJwJsfJkzq6zAARcgUJKbdMGgSVS3i0tgNel6XgeKWg8o7JbVR7/ums6AOsAAAA==" + ] + } + }, + { + "outpoint": { + "txid": "766fc46ba5c2da41cd4c4bc0566e0f4e0f24c184c41acd3bead5cd7b11120367", + "vout": 0 + }, + "receiver": { + "address": "tark1qgw4gpt40zet7q399hv78z7pdak5sxlcgzhy6y6qq7hw3syeudc6xqsws4tegt5r88eahx7g5try2ua4n9rflsncpresjfwcrq80k0d3systnm98", + "amount": "2000" + }, + "spent": true, + "poolTxid": "", + "spentBy": "d6684a5b9e6939dccdf07d1f0eaf7fdd7b31de4d123e63e400d23de739800d4e", + "expireAt": "1726486389", + "swept": false, + "pending": true, + "pendingData": { + "redeemTx": "cHNidP8BAIkCAAAAARH6LJRGP/pFIkD/o5bBp8fXAhjl8yjfN7MhJsxdt5lrAQAAAAD/////AtAHAAAAAAAAIlEguuBh3KQUVZp+NHV2sixQ/mrsngCuLCGXzsgJPC1FzY7ANQ8AAAAAACJRIP7uXXtl4jLUcVVQU+sX7WFXmx2H6iCMzn7gye0v1Y8JAAAAAAABAStYPg8AAAAAACJRIP7uXXtl4jLUcVVQU+sX7WFXmx2H6iCMzn7gye0v1Y8JIgYCHVQFdXiyvwIlLdnji8FvbUgb+ECuTRNAB67owJnjcaMYAAAAAFYAAIAAAACAAQAAgAAAAAAAAAAAQRSc1yjQ/vHRHev23fKGANLvbOhkNYmGtmRWt8fSszlOJUzRFbnfxd1fq9gIEpaI0vrZww8tlZ94iEL75QoaIbVqQJsPdLYf7fAoXO82VoqwYHu1WevE4g6LxUGBPzfd96q5EEZkoW5qqg+v5dWJUEY467Q6qZLFHwziUaB3KEY8yEpCFcBQkpt0waBJVLeLS2A16XpeB4paDyjsltVHv+6azoA6wO5D2Mh3x0XNGxFCS67GNughkENFodpFeVpZjn76chI8RSAdVAV1eLK/AiUt2eOLwW9tSBv4QK5NE0AHrujAmeNxo60gnNco0P7x0R3r9t3yhgDS72zoZDWJhrZkVrfH0rM5TiWswCEWHVQFdXiyvwIlLdnji8FvbUgb+ECuTRNAB67owJnjcaM5AUzRFbnfxd1fq9gIEpaI0vrZww8tlZ94iEL75QoaIbVqAAAAAFYAAIAAAACAAQAAgAAAAAAAAAAAARcgUJKbdMGgSVS3i0tgNel6XgeKWg8o7JbVR7/ums6AOsAAAAA=", + "unconditionalForfeitTxs": [ + "cHNidP8BAFICAAAAARH6LJRGP/pFIkD/o5bBp8fXAhjl8yjfN7MhJsxdt5lrAQAAAAD/////AZA9DwAAAAAAFgAU+9NJhjFhe8jX1hrXh3NvDyHZ1cYAAAAAAAEBK1g+DwAAAAAAIlEg/u5de2XiMtRxVVBT6xftYVebHYfqIIzOfuDJ7S/VjwlBFJzXKND+8dEd6/bd8oYA0u9s6GQ1iYa2ZFa3x9KzOU4lTNEVud/F3V+r2AgSlojS+tnDDy2Vn3iIQvvlChohtWpARJzBjlEkN/kTpyFEtpvP2Ui7ypevuxb9J/NUAwhYf8Pmnnj1l3WuKCSi4Fcp1O+lQjIiZlNpwY6J73q/V8Fe2kIVwFCSm3TBoElUt4tLYDXpel4HiloPKOyW1Ue/7prOgDrA7kPYyHfHRc0bEUJLrsY26CGQQ0Wh2kV5WlmOfvpyEjxFIB1UBXV4sr8CJS3Z44vBb21IG/hArk0TQAeu6MCZ43GjrSCc1yjQ/vHRHev23fKGANLvbOhkNYmGtmRWt8fSszlOJazAARcgUJKbdMGgSVS3i0tgNel6XgeKWg8o7JbVR7/ums6AOsAAAA==" + ] + } + } + ] + }` + bobAfterSendingAsync = ` + { + "spendableVtxos": [ + { + "outpoint": { + "txid": "23c3a885f0ea05f7bdf83f3bf7f8ac9dc3f791ad292f4e63a6f53fa5e4935ab0", + "vout": 0 + }, + "receiver": { + "address": "tark1qwnakvl59d5wckz9lqhhdav0uvns6uu3zkc6hg65gh0kgh6wve9pwqa8vzms5xcr7pqgt0sw88vc287dse5rw6fnxuk9f08frf8amxjcrya0tkgt", + "amount": "900" + }, + "spent": false, + "poolTxid": "", + "spentBy": "", + "expireAt": "1726503895", + "swept": false, + "pending": true, + "pendingData": { + "redeemTx": "cHNidP8BAIkCAAAAAdOK9YzYw1ceJznqJxtRXGe0KeHj6CLcLtqLVwcbMCivAAAAAAD/////ArgLAAAAAAAAIlEgC39Vxhw3dIa4heHgFS6X4XwDl1mBggsKLVTBwF1h3qEgegEAAAAAACJRIMkktfIFxFNTtAmy3K0p+7JqVn2kcA0P6y2vJ1QX2zysAAAAAAABASughgEAAAAAACJRIMkktfIFxFNTtAmy3K0p+7JqVn2kcA0P6y2vJ1QX2zysIgYDjGeMfnNwCrU45iB3iRqiFdWTADaiJ968+w3ruFuq1F0YAAAAAFYAAIAAAACAAQAAgAAAAAAAAAAAQRTYEOuHJ0hyLBGzY8nSHpD2F1nby5/XQ5Sh2Je+cQ5Wsx0ZucLmB/LLspxMRN9JcJn3Q2KJRMhhg7415cCg1d0gQNSvgaBk/1WLYqQxCKxCfv8ViVJ7vjBxvNO5tc2FEDy27V9cIrfL1jPJoVrhgPZT0GwY7dkVZS7saIKI03CbipBCFcBQkpt0waBJVLeLS2A16XpeB4paDyjsltVHv+6azoA6wPKiQ0JM6aw2kcUByijEbOydM3gTIVCGN/69q+dmyxcqRSCMZ4x+c3AKtTjmIHeJGqIV1ZMANqIn3rz7Deu4W6rUXa0g2BDrhydIciwRs2PJ0h6Q9hdZ28uf10OUodiXvnEOVrOswCEWjGeMfnNwCrU45iB3iRqiFdWTADaiJ968+w3ruFuq1F05AR0ZucLmB/LLspxMRN9JcJn3Q2KJRMhhg7415cCg1d0gAAAAAFYAAIAAAACAAQAAgAAAAAAAAAAAARcgUJKbdMGgSVS3i0tgNel6XgeKWg8o7JbVR7/ums6AOsAAAAA=", + "unconditionalForfeitTxs": [ + "cHNidP8BAFICAAAAAdOK9YzYw1ceJznqJxtRXGe0KeHj6CLcLtqLVwcbMCivAAAAAAD/////AdiFAQAAAAAAFgAUlsBYsQa9BEiB8ZumuN4J50lbQIoAAAAAAAEBK6CGAQAAAAAAIlEgySS18gXEU1O0CbLcrSn7smpWfaRwDQ/rLa8nVBfbPKxBFNgQ64cnSHIsEbNjydIekPYXWdvLn9dDlKHYl75xDlazHRm5wuYH8suynExE30lwmfdDYolEyGGDvjXlwKDV3SBAZadgbU8gCDvq3XN0EeLIwGKGSAYHZRkGbAnr9ZjCHGKAQlfFNYS0af1Lz4j7Th2osVY8JJv7O736sC5NNQome0IVwFCSm3TBoElUt4tLYDXpel4HiloPKOyW1Ue/7prOgDrA8qJDQkzprDaRxQHKKMRs7J0zeBMhUIY3/r2r52bLFypFIIxnjH5zcAq1OOYgd4kaohXVkwA2oifevPsN67hbqtRdrSDYEOuHJ0hyLBGzY8nSHpD2F1nby5/XQ5Sh2Je+cQ5Ws6zAARcgUJKbdMGgSVS3i0tgNel6XgeKWg8o7JbVR7/ums6AOsAAAA==" + ] + } + } + ], + "spentVtxos": [ + { + "outpoint": { + "txid": "94fa598302f17f00c8881e742ec0ce2f8c8d16f3d54fe6ba0fb7d13a493d84ad", + "vout": 0 + }, + "receiver": { + "address": "tark1qwnakvl59d5wckz9lqhhdav0uvns6uu3zkc6hg65gh0kgh6wve9pwqa8vzms5xcr7pqgt0sw88vc287dse5rw6fnxuk9f08frf8amxjcrya0tkgt", + "amount": "1000" + }, + "spent": true, + "poolTxid": "", + "spentBy": "d6684a5b9e6939dccdf07d1f0eaf7fdd7b31de4d123e63e400d23de739800d4e", + "expireAt": "1726054928", + "swept": false, + "pending": true, + "pendingData": { + "redeemTx": "cHNidP8BAIkCAAAAAWwxXUjG5tidFA0LmUljX//jwW6xWaS6HKyRCw5StsxpAAAAAAD/////AugDAAAAAAAAIlEgt2eR8LtqTP7yUcQtSydeGrRiHnVmHHnZwYjdC23G7MZwSQAAAAAAACJRIKfUzf/o9h+r0v9y4nmyOt9qO8EkDumQPQZGTbEv8fSFAAAAAAABASsgTgAAAAAAACJRIKfUzf/o9h+r0v9y4nmyOt9qO8EkDumQPQZGTbEv8fSFIgYDp9sz9Cto7FhF+C929Y/jJw1zkRWxq6NURd9kX05mShcYAAAAAFYAAIAAAACAAQAAgAAAAAAAAAAAQRSvBIBey3T0IV353FkuGLMmMJFpqHTrliIsJwJsfJkzq7J0B8bQ0j9842h5lUfOWcbj2TeoFx6OCpgoHIqWIBhHQAFqkBLiRmP3AZ8MS77s1QIWZswMV3L72D9gN0f0MbD6XHkmzZeC1clF3uzxr+13wsF0vcFe29Zl3e2gAhMNGYVCFcFQkpt0waBJVLeLS2A16XpeB4paDyjsltVHv+6azoA6wKRtST8P7teUpSF4DAEbfJj5OIXITx5QGbZns/AtxqGyRSCn2zP0K2jsWEX4L3b1j+MnDXORFbGro1RF32RfTmZKF60grwSAXst09CFd+dxZLhizJjCRaah065YiLCcCbHyZM6uswCEWp9sz9Cto7FhF+C929Y/jJw1zkRWxq6NURd9kX05mShc5AbJ0B8bQ0j9842h5lUfOWcbj2TeoFx6OCpgoHIqWIBhHAAAAAFYAAIAAAACAAQAAgAAAAAAAAAAAARcgUJKbdMGgSVS3i0tgNel6XgeKWg8o7JbVR7/ums6AOsAAAAA=", + "unconditionalForfeitTxs": [ + "cHNidP8BAFICAAAAAWwxXUjG5tidFA0LmUljX//jwW6xWaS6HKyRCw5StsxpAAAAAAD/////AVhNAAAAAAAAFgAUSU38/3Mzx5BdILG4oUO+JoHcoT8AAAAAAAEBKyBOAAAAAAAAIlEgp9TN/+j2H6vS/3LiebI632o7wSQO6ZA9BkZNsS/x9IVBFK8EgF7LdPQhXfncWS4YsyYwkWmodOuWIiwnAmx8mTOrsnQHxtDSP3zjaHmVR85ZxuPZN6gXHo4KmCgcipYgGEdAjH8Mg1Z3GdjGzp78Mg2xq1fop9KDfeji+xoyMgYS7q0Nl0AGOAaNzkDRW4cNcefll5jZC2i3nfygKdXsUsR+LEIVwVCSm3TBoElUt4tLYDXpel4HiloPKOyW1Ue/7prOgDrApG1JPw/u15SlIXgMARt8mPk4hchPHlAZtmez8C3GobJFIKfbM/QraOxYRfgvdvWP4ycNc5EVsaujVEXfZF9OZkoXrSCvBIBey3T0IV353FkuGLMmMJFpqHTrliIsJwJsfJkzq6zAARcgUJKbdMGgSVS3i0tgNel6XgeKWg8o7JbVR7/ums6AOsAAAA==" + ] + } + }, + { + "outpoint": { + "txid": "766fc46ba5c2da41cd4c4bc0566e0f4e0f24c184c41acd3bead5cd7b11120367", + "vout": 0 + }, + "receiver": { + "address": "tark1qgw4gpt40zet7q399hv78z7pdak5sxlcgzhy6y6qq7hw3syeudc6xqsws4tegt5r88eahx7g5try2ua4n9rflsncpresjfwcrq80k0d3systnm98", + "amount": "2000" + }, + "spent": true, + "poolTxid": "", + "spentBy": "d6684a5b9e6939dccdf07d1f0eaf7fdd7b31de4d123e63e400d23de739800d4e", + "expireAt": "1726486389", + "swept": false, + "pending": true, + "pendingData": { + "redeemTx": "cHNidP8BAIkCAAAAARH6LJRGP/pFIkD/o5bBp8fXAhjl8yjfN7MhJsxdt5lrAQAAAAD/////AtAHAAAAAAAAIlEguuBh3KQUVZp+NHV2sixQ/mrsngCuLCGXzsgJPC1FzY7ANQ8AAAAAACJRIP7uXXtl4jLUcVVQU+sX7WFXmx2H6iCMzn7gye0v1Y8JAAAAAAABAStYPg8AAAAAACJRIP7uXXtl4jLUcVVQU+sX7WFXmx2H6iCMzn7gye0v1Y8JIgYCHVQFdXiyvwIlLdnji8FvbUgb+ECuTRNAB67owJnjcaMYAAAAAFYAAIAAAACAAQAAgAAAAAAAAAAAQRSc1yjQ/vHRHev23fKGANLvbOhkNYmGtmRWt8fSszlOJUzRFbnfxd1fq9gIEpaI0vrZww8tlZ94iEL75QoaIbVqQJsPdLYf7fAoXO82VoqwYHu1WevE4g6LxUGBPzfd96q5EEZkoW5qqg+v5dWJUEY467Q6qZLFHwziUaB3KEY8yEpCFcBQkpt0waBJVLeLS2A16XpeB4paDyjsltVHv+6azoA6wO5D2Mh3x0XNGxFCS67GNughkENFodpFeVpZjn76chI8RSAdVAV1eLK/AiUt2eOLwW9tSBv4QK5NE0AHrujAmeNxo60gnNco0P7x0R3r9t3yhgDS72zoZDWJhrZkVrfH0rM5TiWswCEWHVQFdXiyvwIlLdnji8FvbUgb+ECuTRNAB67owJnjcaM5AUzRFbnfxd1fq9gIEpaI0vrZww8tlZ94iEL75QoaIbVqAAAAAFYAAIAAAACAAQAAgAAAAAAAAAAAARcgUJKbdMGgSVS3i0tgNel6XgeKWg8o7JbVR7/ums6AOsAAAAA=", + "unconditionalForfeitTxs": [ + "cHNidP8BAFICAAAAARH6LJRGP/pFIkD/o5bBp8fXAhjl8yjfN7MhJsxdt5lrAQAAAAD/////AZA9DwAAAAAAFgAU+9NJhjFhe8jX1hrXh3NvDyHZ1cYAAAAAAAEBK1g+DwAAAAAAIlEg/u5de2XiMtRxVVBT6xftYVebHYfqIIzOfuDJ7S/VjwlBFJzXKND+8dEd6/bd8oYA0u9s6GQ1iYa2ZFa3x9KzOU4lTNEVud/F3V+r2AgSlojS+tnDDy2Vn3iIQvvlChohtWpARJzBjlEkN/kTpyFEtpvP2Ui7ypevuxb9J/NUAwhYf8Pmnnj1l3WuKCSi4Fcp1O+lQjIiZlNpwY6J73q/V8Fe2kIVwFCSm3TBoElUt4tLYDXpel4HiloPKOyW1Ue/7prOgDrA7kPYyHfHRc0bEUJLrsY26CGQQ0Wh2kV5WlmOfvpyEjxFIB1UBXV4sr8CJS3Z44vBb21IG/hArk0TQAeu6MCZ43GjrSCc1yjQ/vHRHev23fKGANLvbOhkNYmGtmRWt8fSszlOJazAARcgUJKbdMGgSVS3i0tgNel6XgeKWg8o7JbVR7/ums6AOsAAAA==" + ] + } + }, + { + "outpoint": { + "txid": "11cba4cbb06290fb7426157efe439940e1e4143d51bdd20567d7bfd28f0d9090", + "vout": 0 + }, + "receiver": { + "address": "tark1qgw4gpt40zet7q399hv78z7pdak5sxlcgzhy6y6qq7hw3syeudc6xqsws4tegt5r88eahx7g5try2ua4n9rflsncpresjfwcrq80k0d3systnm98", + "amount": "3000" + }, + "spent": false, + "poolTxid": "d6684a5b9e6939dccdf07d1f0eaf7fdd7b31de4d123e63e400d23de739800d4e", + "spentBy": "23c3a885f0ea05f7bdf83f3bf7f8ac9dc3f791ad292f4e63a6f53fa5e4935ab0", + "expireAt": "1726503895", + "swept": false, + "pending": false, + "pendingData": null + } + ] + }` +) diff --git a/pkg/client-sdk/covenant_client.go b/pkg/client-sdk/covenant_client.go index 590c570..a9b6895 100644 --- a/pkg/client-sdk/covenant_client.go +++ b/pkg/client-sdk/covenant_client.go @@ -6,6 +6,7 @@ import ( "encoding/hex" "fmt" "math" + "sort" "strings" "sync" "time" @@ -509,6 +510,101 @@ func (a *covenantArkClient) Claim(ctx context.Context) (string, error) { return a.selfTransferAllPendingPayments(ctx, boardingUtxos, receiver, desc) } +func (a *covenantArkClient) GetTransactionHistory(ctx context.Context) ([]Transaction, error) { + spendableVtxos, spentVtxos, err := a.ListVtxos(ctx) + if err != nil { + return nil, err + } + + config, err := a.store.GetData(ctx) + if err != nil { + return nil, err + } + + return vtxosToTxsCovenant(config.RoundLifetime, spendableVtxos, spentVtxos) +} + +func vtxosToTxsCovenant(roundLifetime int64, spendable, spent []client.Vtxo) ([]Transaction, error) { + transactions := make([]Transaction, 0) + + for _, v := range append(spendable, spent...) { + // get vtxo amount + amount := int(v.Amount) + if v.Pending { + // find other spent vtxos that spent this one + relatedVtxos := findVtxosBySpentBy(spent, v.Txid) + for _, r := range relatedVtxos { + if r.Amount < math.MaxInt64 { + rAmount := int(r.Amount) + amount -= rAmount + } + } + } else { + // an onboarding tx has pending false and no pending true related txs + relatedVtxos := findVtxosBySpentBy(spent, v.RoundTxid) + if len(relatedVtxos) > 0 { // not an onboard tx, ignore + continue + } + } // what kind of tx was this? send or receive? + txType := TxReceived + if amount < 0 { + txType = TxSent + } + // check if is a pending tx + pending := false + claimed := true + if len(v.RoundTxid) == 0 && len(v.SpentBy) == 0 { + pending = true + claimed = false + } + redeemTxid := "" + if len(v.RedeemTx) > 0 { + txid, err := getRedeemTxidCovenant(v.RedeemTx) + if err != nil { + return nil, err + } + redeemTxid = txid + } + + // add transaction + transactions = append(transactions, Transaction{ + RoundTxid: v.RoundTxid, + RedeemTxid: redeemTxid, + Amount: uint64(math.Abs(float64(amount))), + Type: txType, + Pending: pending, + Claimed: claimed, + CreatedAt: getCreatedAtFromExpiry(roundLifetime, *v.ExpiresAt), + }) + } + + // Sort the slice by age + sort.Slice(transactions, func(i, j int) bool { + txi := transactions[i] + txj := transactions[j] + if txi.CreatedAt.Equal(txj.CreatedAt) { + return txi.Type > txj.Type + } + return txi.CreatedAt.After(txj.CreatedAt) + }) + + return transactions, nil +} + +func getRedeemTxidCovenant(redeemTx string) (string, error) { + redeemPtx, err := psetv2.NewPsetFromBase64(redeemTx) + if err != nil { + return "", fmt.Errorf("failed to parse redeem tx: %s", err) + } + + tx, err := redeemPtx.UnsignedTx() + if err != nil { + return "", fmt.Errorf("failed to get txid from redeem tx: %s", err) + } + + return tx.TxHash().String(), nil +} + func (a *covenantArkClient) getClaimableBoardingUtxos(ctx context.Context) ([]explorer.Utxo, error) { offchainAddrs, boardingAddrs, _, err := a.wallet.GetAddresses(ctx) if err != nil { diff --git a/pkg/client-sdk/covenantless_client.go b/pkg/client-sdk/covenantless_client.go index a4bd8ca..b7d7bb0 100644 --- a/pkg/client-sdk/covenantless_client.go +++ b/pkg/client-sdk/covenantless_client.go @@ -6,6 +6,7 @@ import ( "encoding/hex" "fmt" "math" + "sort" "strings" "sync" "time" @@ -633,6 +634,96 @@ func (a *covenantlessArkClient) Claim(ctx context.Context) (string, error) { return a.selfTransferAllPendingPayments(ctx, pendingVtxos, boardingUtxos, receiver, desc) } +func (a *covenantlessArkClient) GetTransactionHistory(ctx context.Context) ([]Transaction, error) { + spendableVtxos, spentVtxos, err := a.ListVtxos(ctx) + if err != nil { + return nil, err + } + + config, err := a.store.GetData(ctx) + if err != nil { + return nil, err + } + + return vtxosToTxsCovenantless(config.RoundLifetime, spendableVtxos, spentVtxos) +} + +func vtxosToTxsCovenantless(roundLifetime int64, spendable, spent []client.Vtxo) ([]Transaction, error) { + transactions := make([]Transaction, 0) + + for _, v := range append(spendable, spent...) { + // get vtxo amount + amount := int(v.Amount) + if v.Pending { + // find other spent vtxos that spent this one + relatedVtxos := findVtxosBySpentBy(spent, v.Txid) + for _, r := range relatedVtxos { + if r.Amount < math.MaxInt64 { + rAmount := int(r.Amount) + amount -= rAmount + } + } + } else { + // an onboarding tx has pending false and no pending true related txs + relatedVtxos := findVtxosBySpentBy(spent, v.RoundTxid) + if len(relatedVtxos) > 0 { // not an onboard tx, ignore + continue + } + } // what kind of tx was this? send or receive? + txType := TxReceived + if amount < 0 { + txType = TxSent + } + // check if is a pending tx + pending := false + claimed := true + if len(v.RoundTxid) == 0 && len(v.SpentBy) == 0 { + pending = true + claimed = false + } + redeemTxid := "" + if len(v.RedeemTx) > 0 { + txid, err := getRedeemTxidCovenantless(v.RedeemTx) + if err != nil { + return nil, err + } + redeemTxid = txid + } + + // add transaction + transactions = append(transactions, Transaction{ + RoundTxid: v.RoundTxid, + RedeemTxid: redeemTxid, + Amount: uint64(math.Abs(float64(amount))), + Type: txType, + Pending: pending, + Claimed: claimed, + CreatedAt: getCreatedAtFromExpiry(roundLifetime, *v.ExpiresAt), + }) + } + + // Sort the slice by age + sort.Slice(transactions, func(i, j int) bool { + txi := transactions[i] + txj := transactions[j] + if txi.CreatedAt.Equal(txj.CreatedAt) { + return txi.Type > txj.Type + } + return txi.CreatedAt.After(txj.CreatedAt) + }) + + return transactions, nil +} + +func getRedeemTxidCovenantless(redeemTx string) (string, error) { + redeemPtx, err := psbt.NewFromRawBytes(strings.NewReader(redeemTx), true) + if err != nil { + return "", fmt.Errorf("failed to parse redeem tx: %s", err) + } + + return redeemPtx.UnsignedTx.TxID(), nil +} + func (a *covenantlessArkClient) sendOnchain( ctx context.Context, receivers []Receiver, ) (string, error) { @@ -1584,8 +1675,6 @@ func (a *covenantlessArkClient) selfTransferAllPendingPayments( } for _, utxo := range boardingUtxo { - fmt.Println(utxo) - fmt.Println(boardingDescriptor) inputs = append(inputs, client.BoardingInput{ VtxoKey: client.VtxoKey{ Txid: utxo.Txid, @@ -1623,3 +1712,12 @@ func (a *covenantlessArkClient) selfTransferAllPendingPayments( return roundTxid, nil } + +func findVtxosBySpentBy(allVtxos []client.Vtxo, txid string) (vtxos []client.Vtxo) { + for _, v := range allVtxos { + if v.SpentBy == txid { + vtxos = append(vtxos, v) + } + } + return +} diff --git a/pkg/client-sdk/example/covenant/alice_to_bob.go b/pkg/client-sdk/example/covenant/alice_to_bob.go index d28ee6b..1ef228c 100644 --- a/pkg/client-sdk/example/covenant/alice_to_bob.go +++ b/pkg/client-sdk/example/covenant/alice_to_bob.go @@ -195,18 +195,18 @@ func runCommand(name string, arg ...string) (string, error) { wg.Wait() if err := cmd.Wait(); err != nil { if errMsg := errorb.String(); len(errMsg) > 0 { - return "", fmt.Errorf(errMsg) + return "", fmt.Errorf("failed cmd wait: %v", errMsg) } if outMsg := output.String(); len(outMsg) > 0 { - return "", fmt.Errorf(outMsg) + return "", fmt.Errorf("failed reading output: %v", outMsg) } return "", err } if errMsg := errb.String(); len(errMsg) > 0 { - return "", fmt.Errorf(errMsg) + return "", fmt.Errorf("run cmd failed: %v", errMsg) } return strings.Trim(output.String(), "\n"), nil diff --git a/pkg/client-sdk/example/covenantless/alice_to_bob.go b/pkg/client-sdk/example/covenantless/alice_to_bob.go index ebf9056..3755da8 100644 --- a/pkg/client-sdk/example/covenantless/alice_to_bob.go +++ b/pkg/client-sdk/example/covenantless/alice_to_bob.go @@ -203,18 +203,18 @@ func runCommand(name string, arg ...string) (string, error) { wg.Wait() if err := cmd.Wait(); err != nil { if errMsg := errorb.String(); len(errMsg) > 0 { - return "", fmt.Errorf(errMsg) + return "", fmt.Errorf("%s", errMsg) } if outMsg := output.String(); len(outMsg) > 0 { - return "", fmt.Errorf(outMsg) + return "", fmt.Errorf("%s", outMsg) } return "", err } if errMsg := errb.String(); len(errMsg) > 0 { - return "", fmt.Errorf(errMsg) + return "", fmt.Errorf("%s", errMsg) } return strings.Trim(output.String(), "\n"), nil diff --git a/pkg/client-sdk/explorer/explorer.go b/pkg/client-sdk/explorer/explorer.go index d435534..00ba5a4 100644 --- a/pkg/client-sdk/explorer/explorer.go +++ b/pkg/client-sdk/explorer/explorer.go @@ -188,7 +188,7 @@ func (e *explorerSvc) GetUtxos(addr string) ([]ExplorerUtxo, error) { return nil, err } if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf(string(body)) + return nil, fmt.Errorf("failed to get utxos: %s", string(body)) } payload := []ExplorerUtxo{} if err := json.Unmarshal(body, &payload); err != nil { @@ -257,7 +257,7 @@ func (e *explorerSvc) GetTxBlockTime( } if resp.StatusCode != http.StatusOK { - return false, 0, fmt.Errorf(string(body)) + return false, 0, fmt.Errorf("failed to get block time: %s", string(body)) } var tx struct { @@ -290,7 +290,7 @@ func (e *explorerSvc) getTxHex(txid string) (string, error) { } if resp.StatusCode != http.StatusOK { - return "", fmt.Errorf(string(body)) + return "", fmt.Errorf("failed to get tx hex: %s", string(body)) } hex := string(body) @@ -312,7 +312,7 @@ func (e *explorerSvc) broadcast(txHex string) (string, error) { } if resp.StatusCode != http.StatusOK { - return "", fmt.Errorf(string(bodyResponse)) + return "", fmt.Errorf("failed to broadcast: %s", string(bodyResponse)) } return string(bodyResponse), nil diff --git a/pkg/client-sdk/go.mod b/pkg/client-sdk/go.mod index 473bd75..59caa88 100644 --- a/pkg/client-sdk/go.mod +++ b/pkg/client-sdk/go.mod @@ -1,6 +1,6 @@ module github.com/ark-network/ark/pkg/client-sdk -go 1.22.6 +go 1.23.1 replace github.com/ark-network/ark/common => ../../common diff --git a/pkg/client-sdk/internal/utils/utils.go b/pkg/client-sdk/internal/utils/utils.go index e2ee316..5b8fb80 100644 --- a/pkg/client-sdk/internal/utils/utils.go +++ b/pkg/client-sdk/internal/utils/utils.go @@ -221,6 +221,7 @@ func DecryptAES128(encrypted, password []byte) ([]byte, error) { return nil, err } nonce, text := data[:gcm.NonceSize()], data[gcm.NonceSize():] + // #nosec G407 plaintext, err := gcm.Open(nil, nonce, text, nil) if err != nil { return nil, fmt.Errorf("invalid password") diff --git a/pkg/client-sdk/types.go b/pkg/client-sdk/types.go index 70b2dbd..bcb0bc9 100644 --- a/pkg/client-sdk/types.go +++ b/pkg/client-sdk/types.go @@ -2,6 +2,7 @@ package arksdk import ( "fmt" + "time" "github.com/ark-network/ark/common" grpcclient "github.com/ark-network/ark/pkg/client-sdk/client/grpc" @@ -129,3 +130,20 @@ type balanceRes struct { offchainBalanceByExpiration map[int64]uint64 err error } + +const ( + TxSent TxType = "sent" + TxReceived TxType = "received" +) + +type TxType string + +type Transaction struct { + RoundTxid string + RedeemTxid string + Amount uint64 + Type TxType + Pending bool + Claimed bool + CreatedAt time.Time +} diff --git a/server/cmd/arkd/commands.go b/server/cmd/arkd/commands.go index 3e91fef..1f29f45 100644 --- a/server/cmd/arkd/commands.go +++ b/server/cmd/arkd/commands.go @@ -237,7 +237,7 @@ func post[T any](url, body, key, macaroon, tlsCert string) (result T, err error) return } if resp.StatusCode != http.StatusOK { - err = fmt.Errorf(string(buf)) + err = fmt.Errorf("failed to post: %s", string(buf)) return } if key == "" { @@ -283,7 +283,7 @@ func get[T any](url, key, macaroon, tlsCert string) (result T, err error) { return } if resp.StatusCode != http.StatusOK { - err = fmt.Errorf(string(buf)) + err = fmt.Errorf("failed to get: %s", string(buf)) return } @@ -347,7 +347,7 @@ func getBalance(url, macaroon, tlsCert string) (*balance, error) { return nil, err } if resp.StatusCode != http.StatusOK { - err = fmt.Errorf(string(buf)) + err = fmt.Errorf("%s", buf) return nil, err } @@ -401,7 +401,7 @@ func getStatus(url, tlsCert string) (*status, error) { } if resp.StatusCode != http.StatusOK { - err = fmt.Errorf(string(buf)) + err = fmt.Errorf("failed to get status: %s", string(buf)) return nil, err } diff --git a/server/go.mod b/server/go.mod index 7b8d5d1..407f66b 100644 --- a/server/go.mod +++ b/server/go.mod @@ -1,6 +1,6 @@ module github.com/ark-network/ark/server -go 1.22.6 +go 1.23.1 replace github.com/ark-network/ark/common => ../common diff --git a/server/internal/config/config.go b/server/internal/config/config.go index cef1971..1d91b21 100644 --- a/server/internal/config/config.go +++ b/server/internal/config/config.go @@ -58,13 +58,15 @@ var ( BoardingExitDelay = "BOARDING_EXIT_DELAY" EsploraURL = "ESPLORA_URL" NeutrinoPeer = "NEUTRINO_PEER" - BitcoindRpcUser = "BITCOIND_RPC_USER" - BitcoindRpcPass = "BITCOIND_RPC_PASS" - BitcoindRpcHost = "BITCOIND_RPC_HOST" - NoMacaroons = "NO_MACAROONS" - NoTLS = "NO_TLS" - TLSExtraIP = "TLS_EXTRA_IP" - TLSExtraDomain = "TLS_EXTRA_DOMAIN" + // #nosec G101 + BitcoindRpcUser = "BITCOIND_RPC_USER" + // #nosec G101 + BitcoindRpcPass = "BITCOIND_RPC_PASS" + BitcoindRpcHost = "BITCOIND_RPC_HOST" + NoMacaroons = "NO_MACAROONS" + NoTLS = "NO_TLS" + TLSExtraIP = "TLS_EXTRA_IP" + TLSExtraDomain = "TLS_EXTRA_DOMAIN" defaultDatadir = common.AppDataDir("arkd", false) defaultRoundInterval = 5 diff --git a/server/test/e2e/test_utils.go b/server/test/e2e/test_utils.go index 284223f..8ce2095 100644 --- a/server/test/e2e/test_utils.go +++ b/server/test/e2e/test_utils.go @@ -80,18 +80,18 @@ func RunCommand(name string, arg ...string) (string, error) { wg.Wait() if err := cmd.Wait(); err != nil { if errMsg := errorb.String(); len(errMsg) > 0 { - return "", fmt.Errorf(errMsg) + return "", fmt.Errorf("%s", errMsg) } if outMsg := output.String(); len(outMsg) > 0 { - return "", fmt.Errorf(outMsg) + return "", fmt.Errorf("%s", outMsg) } return "", err } if errMsg := errb.String(); len(errMsg) > 0 { - return "", fmt.Errorf(errMsg) + return "", fmt.Errorf("%s", errMsg) } return strings.Trim(output.String(), "\n"), nil