Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion .github/workflows/integration.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version: '1.26.1'
go-version: '1.26.2'

- run: go mod tidy

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/unit.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version: '1.26.1'
go-version: '1.26.2'

- run: go mod tidy

Expand Down
73 changes: 73 additions & 0 deletions asset.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ package arksdk

import (
"context"
"fmt"

"github.com/arkade-os/arkd/pkg/ark-lib/asset"
client "github.com/arkade-os/arkd/pkg/client-lib"
clientTypes "github.com/arkade-os/arkd/pkg/client-lib/types"
log "github.com/sirupsen/logrus"
)

func (a *arkClient) IssueAsset(
Expand Down Expand Up @@ -33,9 +35,80 @@ func (a *arkClient) IssueAsset(
return "", nil, err
}

// Persist the issued AssetInfo(s) into the local AssetStore so that
// GetAssetDetails can serve lookups without an indexer round-trip.
a.persistIssuedAssets(ctx, res.IssuedAssets, controlAsset, metadata)

return res.Txid, res.IssuedAssets, nil
}

// persistIssuedAssets writes one AssetInfo entry per freshly issued asset
// id into the local AssetStore. When the caller requested a new control
// asset, the first issued id is the control asset itself — all siblings
// carry its ControlAssetId back-reference. Failures are logged and
// swallowed so they cannot mask a successful on-chain issuance.
func (a *arkClient) persistIssuedAssets(
ctx context.Context, assetIds []asset.AssetId,
controlAsset clientTypes.ControlAsset, metadata []asset.Metadata,
) {
if a.store == nil || len(assetIds) == 0 {
return
}

// Determine the control asset id to link all issued assets to.
var controlAssetId string
switch ca := controlAsset.(type) {
case clientTypes.ExistingControlAsset:
controlAssetId = ca.ID
case clientTypes.NewControlAsset:
// The service returns the control asset first when NewControlAsset
// is requested (see client-lib/asset.go IssueAsset).
if len(assetIds) > 0 {
controlAssetId = assetIds[0].String()
}
}

for i, id := range assetIds {
info := clientTypes.AssetInfo{
AssetId: id.String(),
ControlAssetId: controlAssetId,
Metadata: metadata,
}
// For NewControlAsset, the first id is the control asset itself:
// do not set ControlAssetId on its own row to avoid a self-loop.
if _, isNew := controlAsset.(clientTypes.NewControlAsset); isNew && i == 0 {
info.ControlAssetId = ""
}
if storeErr := a.store.AssetStore().UpsertAsset(ctx, info); storeErr != nil {
log.Warnf(
"failed to persist issued asset info for %s: %v",
id.String(), storeErr,
)
}
}
}

// GetAssetDetails returns the AssetInfo for the given asset id from the
// local AssetStore
// The AssetInfo is populated at issuance time by IssueAsset (see
// persistIssuedAssets above). Callers that need data about assets issued
// by other wallets should query the indexer via Indexer().GetAsset.
func (a *arkClient) GetAssetDetails(
ctx context.Context, assetId string,
) (*clientTypes.AssetInfo, error) {
if err := a.safeCheck(); err != nil {
return nil, err
}
if a.store == nil {
return nil, fmt.Errorf("asset store not initialized")
}
info, err := a.store.AssetStore().GetAsset(ctx, assetId)
if err != nil {
return nil, fmt.Errorf("getting asset details for %s: %w", assetId, err)
}
return info, nil
}

func (a *arkClient) ReissueAsset(
ctx context.Context, assetId string, amount uint64,
) (string, error) {
Expand Down
81 changes: 78 additions & 3 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -314,9 +314,66 @@ func (a *arkClient) GetTransactionHistory(ctx context.Context) ([]clientTypes.Tr
sort.SliceStable(history, func(i, j int) bool {
return history[i].CreatedAt.IsZero() || history[i].CreatedAt.After(history[j].CreatedAt)
})
// Prefer the per-asset breakdown persisted directly on the row (populated
// by vtxosToTxs from indexer-supplied vtxo.Assets). Fall back to deriving
// from AssetPacket for rows where only the packet is available (e.g.
// transactions persisted by wallet-side session/settlement handlers that
// had the signed PSBT in hand).
for i := range history {
if len(history[i].Assets) == 0 {
history[i].Assets = deriveAssetsFromPacket(
history[i].AssetPacket,
history[i].TransactionKey.String(),
)
}
}
Comment thread
louisinger marked this conversation as resolved.
return history, nil
}

// deriveAssetsFromPacket reduces an asset.Packet into a per-asset balance
// summary (one clientTypes.Asset per unique asset id), summing amounts across
// all outputs in each group.
func deriveAssetsFromPacket(pkt asset.Packet, txid string) []clientTypes.Asset {
if len(pkt) == 0 {
return nil
}
sums := make(map[string]uint64)
order := make([]string, 0, len(pkt))
for groupIndex, group := range pkt {
assetId := group.AssetId
// issuance case
if assetId == nil && txid != "" {
derived, err := asset.NewAssetId(txid, uint16(groupIndex))
if err == nil {
assetId = derived
}
}

if assetId == nil {
continue
}
id := assetId.String()
if _, seen := sums[id]; !seen {
order = append(order, id)
}
for _, out := range group.Outputs {
sums[id] += out.Amount
}
}
if len(order) == 0 {
return nil
}

result := make([]clientTypes.Asset, 0, len(order))
for _, id := range order {
result = append(result, clientTypes.Asset{
AssetId: id,
Amount: sums[id],
})
}
return result
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

func (a *arkClient) GetTransactionEventChannel(_ context.Context) <-chan types.TransactionEvent {
if a.txBroadcaster != nil {
return a.txBroadcaster.subscribe(0)
Expand Down Expand Up @@ -1400,7 +1457,6 @@ func (a *arkClient) handleArkTx(
})
}
} else {
// Otherwise, add a new spent tx to the history.
inAmount := uint64(0)
for _, vtxo := range myVtxos {
inAmount += vtxo.Amount
Expand All @@ -1409,12 +1465,23 @@ func (a *arkClient) handleArkTx(
for _, vtxo := range vtxosToAdd {
outAmount += vtxo.Amount
}

var amount uint64
txType := clientTypes.TxSent
switch {
case inAmount > outAmount:
amount = inAmount - outAmount
case outAmount > inAmount:
amount = outAmount - inAmount
txType = clientTypes.TxReceived
}

txsToAdd = append(txsToAdd, clientTypes.Transaction{
TransactionKey: clientTypes.TransactionKey{
ArkTxid: arkTx.Txid,
},
Amount: inAmount - outAmount,
Type: clientTypes.TxSent,
Amount: amount,
Type: txType,
CreatedAt: time.Now(),
AssetPacket: assetPacket,
Hex: arkTx.Tx,
Expand Down Expand Up @@ -1649,6 +1716,10 @@ func (i *arkClient) vtxosToTxs(
Type: clientTypes.TxReceived,
CreatedAt: vtxo.CreatedAt,
SettledBy: settledBy,
Assets: client.NetVtxoAssets(
[]clientTypes.Vtxo{vtxo},
append(settleVtxos, spentVtxos...),
),
})
}

Expand Down Expand Up @@ -1696,6 +1767,7 @@ func (i *arkClient) vtxosToTxs(
Amount: forfeitAmount - resultedAmount,
Type: clientTypes.TxSent,
CreatedAt: vtxo.CreatedAt,
Assets: client.NetVtxoAssets(vtxosBySettledBy[sb], resultedVtxos),
})
}
}
Expand Down Expand Up @@ -1740,6 +1812,7 @@ func (i *arkClient) vtxosToTxs(
Type: clientTypes.TxSent,
CreatedAt: vtxo.CreatedAt,
SettledBy: vtxo.SettledBy,
Assets: client.NetVtxoAssets(vtxosBySpentBy[sb], resultedVtxos),
})
}

Expand Down Expand Up @@ -1916,6 +1989,7 @@ func (a *arkClient) saveSendTransaction(
CreatedAt: createdAt,
Hex: res.Tx,
AssetPacket: res.Extension.GetAssetPacket(),
Assets: client.NetVtxoAssets(res.Inputs, newVtxos),
},
}); err != nil {
log.Warnf("failed to add transactions: %s, skipping adding sent transaction", err)
Expand Down Expand Up @@ -1951,6 +2025,7 @@ func (a *arkClient) saveBatchTransaction(
CreatedAt: time.Now(),
Hex: res.CommitmentTx,
AssetPacket: res.Extension.GetAssetPacket(),
Assets: client.NetVtxoAssets(res.VtxoInputs, res.VtxoOutputs),
},
}); err != nil {
log.Warnf("failed to add sent transaction: %s, skipping", err)
Expand Down
14 changes: 7 additions & 7 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
module github.com/arkade-os/go-sdk

go 1.26.1
go 1.26.2

replace github.com/btcsuite/btcd/btcec/v2 => github.com/btcsuite/btcd/btcec/v2 v2.3.3

require (
github.com/arkade-os/arkd/pkg/ark-lib v0.8.1-0.20260323091657-eeb0baef6937
github.com/arkade-os/arkd/pkg/client-lib v0.0.0-20260403181126-d372608bf69c
github.com/arkade-os/arkd/pkg/ark-lib v0.8.1-0.20260413135435-00ad4e807aa7
github.com/arkade-os/arkd/pkg/client-lib v0.0.0-20260413135435-00ad4e807aa7
github.com/btcsuite/btcd v0.24.3-0.20240921052913-67b8efd3ba53
github.com/btcsuite/btcd/btcec/v2 v2.3.5
github.com/btcsuite/btcd/btcutil v1.1.5
Expand Down Expand Up @@ -92,18 +92,18 @@ require (
go.etcd.io/etcd/raft/v3 v3.5.15 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 // indirect
go.opentelemetry.io/otel v1.40.0 // indirect
go.opentelemetry.io/otel v1.43.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 // indirect
go.opentelemetry.io/otel/metric v1.40.0 // indirect
go.opentelemetry.io/otel/metric v1.43.0 // indirect
go.opentelemetry.io/otel/sdk v1.40.0 // indirect
go.opentelemetry.io/otel/trace v1.40.0 // indirect
go.opentelemetry.io/otel/trace v1.43.0 // indirect
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect
golang.org/x/net v0.51.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.41.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/term v0.40.0 // indirect
golang.org/x/text v0.34.0 // indirect
golang.org/x/time v0.8.0 // indirect
Expand Down
28 changes: 14 additions & 14 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@ github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8
github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g=
github.com/arkade-os/arkd/api-spec v0.0.0-20260323091657-eeb0baef6937 h1:010LTrhQYsGDSA2aE8CoCrOpcZFs1JQ5wPrBomtsfOw=
github.com/arkade-os/arkd/api-spec v0.0.0-20260323091657-eeb0baef6937/go.mod h1:2+6ix1UGGE22Q/rbab1dycFPPKiTyq0gldKVqVfFPWs=
github.com/arkade-os/arkd/pkg/ark-lib v0.8.1-0.20260323091657-eeb0baef6937 h1:1r8NojJwAR/Q5hanPW0PzYJrhjZU5620HRAIqs+rItQ=
github.com/arkade-os/arkd/pkg/ark-lib v0.8.1-0.20260323091657-eeb0baef6937/go.mod h1:P5ipJ1CatvH6xKxEtMUt4KhUGl1JPLcFiSdo1PGTny8=
github.com/arkade-os/arkd/pkg/client-lib v0.0.0-20260403181126-d372608bf69c h1:jXBBMgkkiwtYJv4TmWp2KmTOp9Pqnwo74YauaBh43hw=
github.com/arkade-os/arkd/pkg/client-lib v0.0.0-20260403181126-d372608bf69c/go.mod h1:Y59cvCug7rHWjERYKbbrjStn4+vXGmWu5LOqLggf7Aw=
github.com/arkade-os/arkd/pkg/ark-lib v0.8.1-0.20260413135435-00ad4e807aa7 h1:hEPi51FcHADGG+5vhYPeMG0NLtzxjKzyCZoj4Q+u3Lw=
github.com/arkade-os/arkd/pkg/ark-lib v0.8.1-0.20260413135435-00ad4e807aa7/go.mod h1:gYJkHBV9B9OkSgANCvyST24HTlZ0tSmr1mZSsj+jGAc=
github.com/arkade-os/arkd/pkg/client-lib v0.0.0-20260413135435-00ad4e807aa7 h1:9KNN9nzhccWp2jKCpIUaAxzSwz1UOWN7PPvehwJFVJE=
github.com/arkade-os/arkd/pkg/client-lib v0.0.0-20260413135435-00ad4e807aa7/go.mod h1:StCLdfzfEK2voKaoApMfUAG+P+COMKwYmCps3tZ2VTg=
github.com/arkade-os/arkd/pkg/errors v0.0.0-20260303153651-8615412e4dea h1:x9ZwZL+F2b9E0uBZYBVjCLGtlqIE4zahDOY4C89h3X4=
github.com/arkade-os/arkd/pkg/errors v0.0.0-20260303153651-8615412e4dea/go.mod h1:NYGE+baj57ynbXNwjISJddMDpMqAWOX27dV22xqFm2A=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
Expand Down Expand Up @@ -445,20 +445,20 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 h1:r6I7RJCN86bpD/FQwedZ0vSixDpwuWREjW9oRMsmqDc=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0/go.mod h1:B9yO6b04uB80CzjedvewuqDhxJxi11s7/GtiGa8bAjI=
go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 h1:QKdN8ly8zEMrByybbQgv8cWBcdAarwmIPZ6FThrWXJs=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0/go.mod h1:bTdK1nhqF76qiPoCCdyFIV+N/sRHYXYCTQc+3VCi3MI=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 h1:DvJDOPmSWQHWywQS6lKL+pb8s3gBLOZUtw4N+mavW1I=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0/go.mod h1:EtekO9DEJb4/jRyN4v4Qjc2yA7AtfCBuz2FynRUWTXs=
go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=
go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8=
go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE=
go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw=
go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg=
go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=
go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
Expand Down Expand Up @@ -541,8 +541,8 @@ golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
Expand Down
11 changes: 10 additions & 1 deletion sdk.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,16 @@ type ArkClient interface {
BurnAsset(
ctx context.Context, assetID string, amount uint64,
) (string, error)
SendOffChain(ctx context.Context, receivers []clientTypes.Receiver) (string, error)
SendOffChain(
ctx context.Context, receivers []clientTypes.Receiver, opts ...SendOffChainOption,
) (string, error)
Comment thread
louisinger marked this conversation as resolved.
// GetAssetDetails returns the AssetInfo (id, control asset id, metadata)
// that was persisted to the local AssetStore at issuance time. It queries
// the local store only — it does NOT make an indexer round-trip. Callers
// that need supply or remote-only data should use the Indexer() directly.
//
// Returns an error if the asset is not present in the local store.
GetAssetDetails(ctx context.Context, assetId string) (*clientTypes.AssetInfo, error)
RegisterIntent(
ctx context.Context,
vtxos []clientTypes.Vtxo, boardingUtxos []clientTypes.Utxo, notes []string,
Expand Down
Loading
Loading