Skip to content
Merged
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
1 change: 1 addition & 0 deletions cmd/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ Upgrading:
wallet.BalanceCmd,
wallet.CreateCmd,
wallet.ImportCmd,
wallet.TrackCmd,
wallet.InitCmd,
wallet.ListCmd,
wallet.RemoveCmd,
Expand Down
40 changes: 40 additions & 0 deletions cmd/wallet/track.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package wallet

import (
"github.com/cockroachdb/errors"
"github.com/data-preservation-programs/singularity/cmd/cliutil"
"github.com/data-preservation-programs/singularity/database"
"github.com/data-preservation-programs/singularity/handler/wallet"
"github.com/data-preservation-programs/singularity/util"
"github.com/urfave/cli/v2"
)

var TrackCmd = &cli.Command{
Name: "track",
Usage: "Track a wallet without importing private key",
ArgsUsage: "<actor_id>",
Before: cliutil.CheckNArgs,
Action: func(c *cli.Context) error {
db, closer, err := database.OpenFromCLI(c)
if err != nil {
return errors.WithStack(err)
}
defer func() { _ = closer.Close() }()

actorID := c.Args().Get(0)
lotusClient := util.NewLotusClient(c.String("lotus-api"), c.String("lotus-token"))

request := wallet.CreateRequest{
ActorID: actorID,
TrackOnly: true,
}

w, err := wallet.Default.CreateHandler(c.Context, db, lotusClient, request)
if err != nil {
return errors.WithStack(err)
}

cliutil.Print(c, w)
return nil
},
}
10 changes: 8 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ go 1.23.6
require (
github.com/alecthomas/units v0.0.0-20231202071711-9a357b53e9c9
github.com/avast/retry-go v3.0.0+incompatible
github.com/bcicen/jstream v1.0.1
github.com/brianvoe/gofakeit/v6 v6.23.2
github.com/cockroachdb/errors v1.11.3
github.com/data-preservation-programs/table v0.0.3
Expand Down Expand Up @@ -57,7 +56,6 @@ require (
github.com/libp2p/go-libp2p v0.39.1
github.com/mattn/go-shellwords v1.0.12
github.com/minio/sha256-simd v1.0.1
github.com/mitchellh/mapstructure v1.5.0
github.com/multiformats/go-multiaddr v0.14.0
github.com/multiformats/go-multicodec v0.9.0
github.com/multiformats/go-multihash v0.2.3
Expand All @@ -72,6 +70,7 @@ require (
github.com/stretchr/testify v1.10.0
github.com/swaggo/echo-swagger v1.4.0
github.com/swaggo/swag v1.16.1
github.com/tidwall/gjson v1.18.0
github.com/urfave/cli/v2 v2.27.3
github.com/ybbus/jsonrpc/v3 v3.1.4
go.mongodb.org/mongo-driver v1.12.1
Expand All @@ -86,9 +85,14 @@ require (
)

require (
github.com/bitfield/gotestdox v0.2.2 // indirect
github.com/dnephin/pflag v1.0.7 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/shirou/gopsutil/v3 v3.23.3 // indirect
golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c // indirect
gotest.tools/gotestsum v1.12.3 // indirect
)

require (
Expand Down Expand Up @@ -328,6 +332,8 @@ require (
github.com/stretchr/objx v0.5.2 // indirect
github.com/swaggo/files/v2 v2.0.0 // indirect
github.com/t3rm1n4l/go-mega v0.0.0-20230228171823-a01a2cda13ca // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
github.com/tklauser/go-sysconf v0.3.11 // indirect
github.com/tklauser/numcpus v0.6.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
Expand Down
16 changes: 14 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,6 @@ github.com/avast/retry-go v3.0.0+incompatible h1:4SOWQ7Qs+oroOTQOYnAHqelpCO0biHS
github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY=
github.com/aws/aws-sdk-go v1.44.332 h1:Ze+98F41+LxoJUdsisAFThV+0yYYLYw17/Vt0++nFYM=
github.com/aws/aws-sdk-go v1.44.332/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
github.com/bcicen/jstream v1.0.1 h1:BXY7Cu4rdmc0rhyTVyT3UkxAiX3bnLpKLas9btbH5ck=
github.com/bcicen/jstream v1.0.1/go.mod h1:9ielPxqFry7Y4Tg3j4BfjPocfJ3TbsRtXOAYXYmRuAQ=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o=
Expand All @@ -97,6 +95,8 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
github.com/bitfield/gotestdox v0.2.2 h1:x6RcPAbBbErKLnapz1QeAlf3ospg8efBsedU93CDsnE=
github.com/bitfield/gotestdox v0.2.2/go.mod h1:D+gwtS0urjBrzguAkTM2wodsTQYFHdpx8eqRJ3N+9pY=
github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g=
github.com/brianvoe/gofakeit/v6 v6.23.2 h1:lVde18uhad5wII/f5RMVFLtdQNE0HaGFuBUXmYKk8i8=
github.com/brianvoe/gofakeit/v6 v6.23.2/go.mod h1:Ow6qC71xtwm79anlwKRlWZW6zVq9D2XHE4QSSMP/rU8=
Expand Down Expand Up @@ -162,6 +162,8 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3
github.com/dlespiau/covertool v0.0.0-20180314162135-b0c4c6d0583a/go.mod h1:/eQMcW3eA1bzKx23ZYI2H3tXPdJB5JWYTHzoUPBvQY4=
github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI=
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
github.com/dnephin/pflag v1.0.7 h1:oxONGlWxhmUct0YzKTgrpQv9AUA1wtPBn7zuSjJqptk=
github.com/dnephin/pflag v1.0.7/go.mod h1:uxE91IoWURlOiTUIA8Mq5ZZkAv3dPUfZNaT80Zm7OQE=
github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8=
github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v24.0.5+incompatible h1:WmgcE4fxyI6EEXxBRxsHnZXrO1pQ3smi0k/jho4HLeY=
Expand Down Expand Up @@ -466,6 +468,8 @@ github.com/google/pprof v0.0.0-20250202011525-fc3143867406/go.mod h1:vavhavw2zAx
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
Expand Down Expand Up @@ -1137,7 +1141,13 @@ github.com/swaggo/swag v1.16.1/go.mod h1:9/LMvHycG3NFHfR6LwvikHv5iFvmPADQ359cKik
github.com/t3rm1n4l/go-mega v0.0.0-20230228171823-a01a2cda13ca h1:I9rVnNXdIkij4UvMT7OmKhH9sOIvS8iXkxfPdnn9wQA=
github.com/t3rm1n4l/go-mega v0.0.0-20230228171823-a01a2cda13ca/go.mod h1:suDIky6yrK07NnaBadCB4sS0CqFOvUK91lH7CR+JlDA=
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tj/go-spin v1.1.0/go.mod h1:Mg1mzmePZm4dva8Qz60H2lHwmJ2loum4VIrLgVnKwh4=
github.com/tklauser/go-sysconf v0.3.11 h1:89WgdJhk5SNwJfu+GKyYveZ4IaJ7xAkecBo+KdJV0CM=
github.com/tklauser/go-sysconf v0.3.11/go.mod h1:GqXfhXY3kiPa0nAXPDIQIWzJbMCB7AmcWpGR8lSZfqI=
Expand Down Expand Up @@ -1763,6 +1773,8 @@ gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
gotest.tools/gotestsum v1.12.3 h1:jFwenGJ0RnPkuKh2VzAYl1mDOJgbhobBDeL2W1iEycs=
gotest.tools/gotestsum v1.12.3/go.mod h1:Y1+e0Iig4xIRtdmYbEV7K7H6spnjc1fX4BOuUhWw2Wk=
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o=
Expand Down
42 changes: 40 additions & 2 deletions handler/wallet/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"strings"

"github.com/cockroachdb/errors"
"github.com/data-preservation-programs/singularity/database"
Expand Down Expand Up @@ -105,6 +107,8 @@
// For SPWallet creation
Address string `json:"address,omitempty"`
ActorID string `json:"actorId,omitempty"`
// For TrackedWallet creation
TrackOnly bool `json:"trackOnly,omitempty"`
// Optional fields for adding details to Wallet
Name string `json:"name,omitempty"`
Contact string `json:"contact,omitempty"`
Expand Down Expand Up @@ -156,12 +160,15 @@
hasKeyType := request.KeyType != ""
hasAddress := request.Address != ""
hasActorID := request.ActorID != ""
isTrackOnly := request.TrackOnly

// Validate that only one wallet type is specified
switch {
case !hasKeyType && !hasAddress && !hasActorID:
case isTrackOnly && (!hasActorID || hasAddress || hasKeyType):
return nil, errors.New("TrackedWallet requires only ActorID (no private key or address)")
case !hasKeyType && !hasAddress && !hasActorID && !isTrackOnly:
return nil, errors.New("must specify either KeyType (for UserWallet) or Address/ActorID (for SPWallet)")
case !hasKeyType && (!hasAddress || !hasActorID):
case !hasKeyType && !isTrackOnly && (!hasAddress || !hasActorID):
return nil, errors.New("must specify both Address and ActorID (for SPWallet)")
case hasKeyType && (hasAddress || hasActorID):
return nil, errors.New("cannot specify both KeyType (for UserWallet) and Address/ActorID (for SPWallet) - please specify parameters for one wallet type")
Expand All @@ -169,7 +176,7 @@

var wallet model.Wallet

if hasKeyType {

Check failure on line 179 in handler/wallet/create.go

View workflow job for this annotation

GitHub Actions / go-check / All

ifElseChain: rewrite if-else to switch statement (gocritic)
// Create UserWallet: generate a new keypair
privateKey, address, err := GenerateKey(request.KeyType)
if err != nil {
Expand All @@ -181,6 +188,37 @@
PrivateKey: privateKey,
WalletType: model.UserWallet,
}
} else if isTrackOnly {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

gocritic suggestion declined to keep changeset minimal and targeted, since other work is being done on other wallet types (SP wallets)

// Create TrackedWallet: resolve ActorID to address using Lotus API
if len(request.ActorID) < 2 || (request.ActorID[:2] != "f0" && request.ActorID[:2] != "t0") {
return nil, errors.Wrap(handlererror.ErrInvalidParameter, "ActorID must start with f0 or t0")
}

var addr string
err := lotusClient.CallFor(ctx, &addr, "Filecoin.StateAccountKey", request.ActorID, nil)
if err != nil {
// Check for specific error types to provide better user guidance
errMsg := err.Error()
if strings.Contains(errMsg, "actor code is not account: storageminer") {
return nil, errors.Wrap(handlererror.ErrInvalidParameter,
fmt.Sprintf("ActorID %s is a storage provider, not a client wallet. Wallet tracking is for client wallets that make deals, not storage providers.", request.ActorID))
}
if strings.Contains(errMsg, "actor code is not account") {
return nil, errors.Wrap(handlererror.ErrInvalidParameter,
fmt.Sprintf("ActorID %s is not a client wallet. Wallet tracking is only for client/account actors that make deals.", request.ActorID))
}
if strings.Contains(errMsg, "actor not found") || strings.Contains(errMsg, "failed to find actor") {
return nil, errors.Wrap(handlererror.ErrInvalidParameter,
fmt.Sprintf("ActorID %s does not exist on the network.", request.ActorID))
}
return nil, errors.Wrap(err, "failed to resolve actor ID to address")
}

wallet = model.Wallet{
ActorID: request.ActorID,
Address: addr,
WalletType: model.TrackedWallet,
}
} else {
// Validate the address and actor ID with Lotus
addr, err := address.NewFromString(request.Address)
Expand Down
92 changes: 92 additions & 0 deletions handler/wallet/create_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package wallet

import (
"context"
"errors"
"testing"

"github.com/data-preservation-programs/singularity/util/testutil"
Expand Down Expand Up @@ -89,6 +90,28 @@ func TestCreateHandler(t *testing.T) {
require.Equal(t, "SPWallet", string(w.WalletType))
})

t.Run("success-tracked-wallet", func(t *testing.T) {
// Create mock client for address resolution with different address
trackMockClient := testutil.NewMockLotusClient()
trackMockClient.SetResponse("Filecoin.StateAccountKey", "f1different-tracked-wallet-address")

w, err := Default.CreateHandler(ctx, db, trackMockClient, CreateRequest{
ActorID: "f0123456", // Different ActorID
TrackOnly: true,
Name: "Test Tracked",
Contact: "test@example.com",
Location: "US",
})
require.NoError(t, err)
require.Equal(t, "f0123456", w.ActorID)
require.Equal(t, "f1different-tracked-wallet-address", w.Address) // Different resolved address
require.Equal(t, "Test Tracked", w.ActorName)
require.Equal(t, "test@example.com", w.ContactInfo)
require.Equal(t, "US", w.Location)
require.Empty(t, w.PrivateKey)
require.Equal(t, "TrackedWallet", string(w.WalletType))
})

t.Run("error-no-parameters", func(t *testing.T) {
_, err := Default.CreateHandler(ctx, db, mockClient, CreateRequest{})
require.Error(t, err)
Expand Down Expand Up @@ -134,5 +157,74 @@ func TestCreateHandler(t *testing.T) {
require.Error(t, err)
require.Contains(t, err.Error(), "cannot specify both KeyType (for UserWallet) and Address/ActorID (for SPWallet)")
})

t.Run("error-tracked-wallet-missing-actorid", func(t *testing.T) {
_, err := Default.CreateHandler(ctx, db, mockClient, CreateRequest{
TrackOnly: true,
})
require.Error(t, err)
require.Contains(t, err.Error(), "TrackedWallet requires only ActorID")
})

t.Run("error-tracked-wallet-with-keytype", func(t *testing.T) {
_, err := Default.CreateHandler(ctx, db, mockClient, CreateRequest{
KeyType: KTSecp256k1.String(),
ActorID: testutil.TestWalletActorID,
TrackOnly: true,
})
require.Error(t, err)
require.Contains(t, err.Error(), "TrackedWallet requires only ActorID")
})

t.Run("error-tracked-wallet-with-address", func(t *testing.T) {
_, err := Default.CreateHandler(ctx, db, mockClient, CreateRequest{
Address: testutil.TestWalletAddr,
ActorID: testutil.TestWalletActorID,
TrackOnly: true,
})
require.Error(t, err)
require.Contains(t, err.Error(), "TrackedWallet requires only ActorID")
})

t.Run("error-tracked-wallet-storage-provider", func(t *testing.T) {
// Mock client that returns storage provider error
spMockClient := testutil.NewMockLotusClient()
spMockClient.SetError("Filecoin.StateAccountKey", errors.New("1: failed to get account actor state for f01000: actor code is not account: storageminer"))

_, err := Default.CreateHandler(ctx, db, spMockClient, CreateRequest{
ActorID: "f01000",
TrackOnly: true,
})
require.Error(t, err)
require.Contains(t, err.Error(), "is a storage provider, not a client wallet")
require.Contains(t, err.Error(), "Wallet tracking is for client wallets that make deals")
})

t.Run("error-tracked-wallet-non-account-actor", func(t *testing.T) {
// Mock client that returns non-account error
nonAccountMockClient := testutil.NewMockLotusClient()
nonAccountMockClient.SetError("Filecoin.StateAccountKey", errors.New("actor code is not account: system"))

_, err := Default.CreateHandler(ctx, db, nonAccountMockClient, CreateRequest{
ActorID: "f00",
TrackOnly: true,
})
require.Error(t, err)
require.Contains(t, err.Error(), "is not a client wallet")
require.Contains(t, err.Error(), "Wallet tracking is only for client/account actors")
})

t.Run("error-tracked-wallet-actor-not-found", func(t *testing.T) {
// Mock client that returns actor not found error
notFoundMockClient := testutil.NewMockLotusClient()
notFoundMockClient.SetError("Filecoin.StateAccountKey", errors.New("actor not found"))

_, err := Default.CreateHandler(ctx, db, notFoundMockClient, CreateRequest{
ActorID: "f0999999999",
TrackOnly: true,
})
require.Error(t, err)
require.Contains(t, err.Error(), "does not exist on the network")
})
})
}
Loading
Loading