Skip to content

Commit

Permalink
Merge pull request #232 from SiaFoundation/nate/default-spend-policy
Browse files Browse the repository at this point in the history
api: disable construction API if spend policy is unset
  • Loading branch information
n8maninger authored Feb 14, 2025
2 parents b428ac2 + 65021ca commit bbf2d00
Show file tree
Hide file tree
Showing 2 changed files with 87 additions and 65 deletions.
46 changes: 41 additions & 5 deletions api/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1361,9 +1361,9 @@ func TestConstructSiacoins(t *testing.T) {
}

wc := c.Wallet(w.ID)
// add an address with no spend policy
err = wc.AddAddress(wallet.Address{
Address: senderAddr,
SpendPolicy: &senderPolicy,
Address: senderAddr,
})
if err != nil {
t.Fatal(err)
Expand All @@ -1376,6 +1376,25 @@ func TestConstructSiacoins(t *testing.T) {
testutil.MineBlocks(t, cm, types.VoidAddress, 1)
waitForBlock(t, cm, ws)

// try to construct a valid transaction with no spend policy
_, err = wc.Construct([]types.SiacoinOutput{
{Value: types.Siacoins(1), Address: receiverAddr},
}, nil, senderAddr)
if !strings.Contains(err.Error(), "no spend policy") {
t.Fatalf("expected error to contain %q, got %q", "no spend policy", err)
}

// add the spend policy
err = wc.AddAddress(wallet.Address{
Address: senderAddr,
SpendPolicy: &types.SpendPolicy{
Type: types.PolicyTypeUnlockConditions(types.StandardUnlockConditions(senderPrivateKey.PublicKey())),
},
})
if err != nil {
t.Fatal(err)
}

// try to construct a transaction with more siafunds than the wallet holds.
// this will lock all of the wallet's siacoins
resp, err := wc.Construct([]types.SiacoinOutput{
Expand Down Expand Up @@ -1684,9 +1703,9 @@ func TestConstructV2Siacoins(t *testing.T) {
}

wc := c.Wallet(w.ID)
// add an address without a spend policy
err = wc.AddAddress(wallet.Address{
Address: senderAddr,
SpendPolicy: &senderPolicy,
Address: senderAddr,
})
if err != nil {
t.Fatal(err)
Expand All @@ -1699,9 +1718,26 @@ func TestConstructV2Siacoins(t *testing.T) {
testutil.MineBlocks(t, cm, types.VoidAddress, 1)
waitForBlock(t, cm, ws)

// try to construct a transaction
resp, err := wc.ConstructV2([]types.SiacoinOutput{
{Value: types.Siacoins(1), Address: receiverAddr},
}, nil, senderAddr)
if !strings.Contains(err.Error(), "no spend policy") {
t.Fatalf("expected spend policy error, got %q", err)
}

// add a spend policy to the address
err = wc.AddAddress(wallet.Address{
Address: senderAddr,
SpendPolicy: &senderPolicy,
})
if err != nil {
t.Fatal(err)
}

// try to construct a transaction with more siafunds than the wallet holds.
// this will lock all of the wallet's Siacoin UTXOs
resp, err := wc.ConstructV2([]types.SiacoinOutput{
resp, err = wc.ConstructV2([]types.SiacoinOutput{
{Value: types.Siacoins(1), Address: receiverAddr},
}, []types.SiafundOutput{
{Value: 100000, Address: senderAddr},
Expand Down
106 changes: 46 additions & 60 deletions api/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -802,17 +802,26 @@ func (s *server) walletsConstructHandler(jc jape.Context) {
})
}

knownAddresses := make(map[types.Address]wallet.Address)
getAddress := func(addr types.Address) (wallet.Address, error) {
knownAddresses := make(map[types.Address]types.UnlockConditions)
getAddressUnlockConditions := func(jc jape.Context, addr types.Address) (types.UnlockConditions, bool) {
if a, ok := knownAddresses[addr]; ok {
return a, nil
return a, true
}
a, err := s.wm.WalletAddress(walletID, addr)
if err != nil {
return wallet.Address{}, err
jc.Error(fmt.Errorf("failed to get address: %w", err), http.StatusInternalServerError)
return types.UnlockConditions{}, false
} else if a.SpendPolicy == nil {
jc.Error(fmt.Errorf("address %q has no spend policy", addr), http.StatusBadRequest)
return types.UnlockConditions{}, false
}
uc, ok := a.SpendPolicy.Type.(types.PolicyTypeUnlockConditions)
if !ok {
jc.Error(fmt.Errorf("address %q has v2-only spend policy", addr), http.StatusBadRequest)
return types.UnlockConditions{}, false
}
knownAddresses[addr] = a
return a, nil
knownAddresses[addr] = types.UnlockConditions(uc)
return knownAddresses[addr], true
}

resp := WalletConstructResponse{
Expand All @@ -829,24 +838,14 @@ func (s *server) walletsConstructHandler(jc jape.Context) {
}

for _, sce := range sces {
addr, err := getAddress(sce.SiacoinOutput.Address)
if err != nil {
jc.Error(fmt.Errorf("failed to get address: %w", err), http.StatusInternalServerError)
uc, ok := getAddressUnlockConditions(jc, sce.SiacoinOutput.Address)
if !ok {
return
}

sci := types.SiacoinInput{
ParentID: sce.ID,
}

if addr.SpendPolicy != nil {
// best effort to fill unlock conditions
uc, ok := addr.SpendPolicy.Type.(types.PolicyTypeUnlockConditions)
if !ok {
jc.Error(fmt.Errorf("address %q only unlock conditions are suppored in v1 transactions", addr.Address), http.StatusBadRequest)
return
}
sci.UnlockConditions = types.UnlockConditions(uc)
ParentID: sce.ID,
UnlockConditions: uc,
}

txn.SiacoinInputs = append(txn.SiacoinInputs, sci)
Expand All @@ -859,24 +858,15 @@ func (s *server) walletsConstructHandler(jc jape.Context) {
}

for _, sfe := range sfes {
addr, err := getAddress(sfe.SiafundOutput.Address)
if err != nil {
jc.Error(fmt.Errorf("failed to get address: %w", err), http.StatusInternalServerError)
uc, ok := getAddressUnlockConditions(jc, sfe.SiafundOutput.Address)
if !ok {
return
}

sfi := types.SiafundInput{
ParentID: sfe.ID,
ClaimAddress: wcr.ChangeAddress,
}
if addr.SpendPolicy != nil {
// best effort to fill unlock conditions
uc, ok := addr.SpendPolicy.Type.(types.PolicyTypeUnlockConditions)
if !ok {
jc.Error(fmt.Errorf("address %q only unlock conditions are suppored in v1 transactions", addr.Address), http.StatusBadRequest)
return
}
sfi.UnlockConditions = types.UnlockConditions(uc)
ParentID: sfe.ID,
UnlockConditions: uc,
ClaimAddress: wcr.ChangeAddress,
}
txn.SiafundInputs = append(txn.SiafundInputs, sfi)
txn.Signatures = append(txn.Signatures, types.TransactionSignature{
Expand Down Expand Up @@ -995,17 +985,23 @@ func (s *server) walletsConstructV2Handler(jc jape.Context) {
})
}

knownAddresses := make(map[types.Address]wallet.Address)
getAddress := func(addr types.Address) (wallet.Address, error) {
knownAddresses := make(map[types.Address]types.SpendPolicy)
getAddressSpendPolicy := func(jc jape.Context, addr types.Address) (types.SpendPolicy, bool) {
if a, ok := knownAddresses[addr]; ok {
return a, nil
return a, true
}
a, err := s.wm.WalletAddress(walletID, addr)
if err != nil {
return wallet.Address{}, err
jc.Error(fmt.Errorf("failed to get address: %w", err), http.StatusInternalServerError)
return types.SpendPolicy{}, false
}

if a.SpendPolicy == nil {
jc.Error(fmt.Errorf("address %q has no spend policy", addr), http.StatusBadRequest)
return types.SpendPolicy{}, false
}
knownAddresses[addr] = a
return a, nil
knownAddresses[addr] = *a.SpendPolicy
return knownAddresses[addr], true
}

resp := WalletConstructV2Response{
Expand All @@ -1026,22 +1022,17 @@ func (s *server) walletsConstructV2Handler(jc jape.Context) {
// guaranteed to have a non-zero Siacoin basis while the Siafund basis will be zero when not
// sending Siafunds.
for _, sfe := range sfes {
addr, err := getAddress(sfe.SiafundOutput.Address)
if err != nil {
jc.Error(fmt.Errorf("failed to get address: %w", err), http.StatusInternalServerError)
sp, ok := getAddressSpendPolicy(jc, sfe.SiafundOutput.Address)
if !ok {
return
}

sfi := types.V2SiafundInput{
Parent: sfe,
ClaimAddress: wcr.ChangeAddress,
}

if addr.SpendPolicy != nil {
// best effort to fill spend policy
sfi.SatisfiedPolicy = types.SatisfiedPolicy{
Policy: *addr.SpendPolicy,
}
SatisfiedPolicy: types.SatisfiedPolicy{
Policy: sp,
},
}
txn.SiafundInputs = append(txn.SiafundInputs, sfi)
}
Expand All @@ -1056,21 +1047,16 @@ func (s *server) walletsConstructV2Handler(jc jape.Context) {
}

for _, sce := range sces {
addr, err := getAddress(sce.SiacoinOutput.Address)
if err != nil {
jc.Error(fmt.Errorf("failed to get address: %w", err), http.StatusInternalServerError)
sp, ok := getAddressSpendPolicy(jc, sce.SiacoinOutput.Address)
if !ok {
return
}

sci := types.V2SiacoinInput{
Parent: sce,
}

if addr.SpendPolicy != nil {
// best effort to fill spend policy
sci.SatisfiedPolicy = types.SatisfiedPolicy{
Policy: *addr.SpendPolicy,
}
SatisfiedPolicy: types.SatisfiedPolicy{
Policy: sp,
},
}
txn.SiacoinInputs = append(txn.SiacoinInputs, sci)
}
Expand Down

0 comments on commit bbf2d00

Please sign in to comment.