Skip to content

Commit e2a0f49

Browse files
authoredNov 3, 2024··
Merge pull request #32 from icon-project/feat/contract-deploy-tx
feat(transaction): implement Type-1 transaction
2 parents e8616b9 + e9f9495 commit e2a0f49

File tree

9 files changed

+664
-0
lines changed

9 files changed

+664
-0
lines changed
 

‎pkg/stacks/constants.go

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ type PayloadType byte
1111

1212
const (
1313
PayloadTypeTokenTransfer PayloadType = 0x00
14+
PayloadTypeSmartContract PayloadType = 0x01
1415
PayloadTypeContractCall PayloadType = 0x02
1516
)
1617

‎pkg/transaction/auth.go

+4
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ func (t *TokenTransferTransaction) GetAuth() *TransactionAuth {
3030
return &t.Auth
3131
}
3232

33+
func (t *SmartContractTransaction) GetAuth() *TransactionAuth {
34+
return &t.Auth
35+
}
36+
3337
func (t *ContractCallTransaction) GetAuth() *TransactionAuth {
3438
return &t.Auth
3539
}

‎pkg/transaction/builder.go

+38
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,44 @@ func MakeSTXTokenTransfer(
221221
return tx, nil
222222
}
223223

224+
func MakeContractDeploy(
225+
contractName string,
226+
codeBody string,
227+
network stacks.StacksNetwork,
228+
senderAddress string,
229+
senderKey []byte,
230+
fee *big.Int,
231+
nonce *big.Int,
232+
) (*SmartContractTransaction, error) {
233+
if contractName == "" || codeBody == "" || len(senderKey) == 0 {
234+
return nil, &CustomError{Message: "Invalid parameters: contractName, codeBody, or senderKey are empty"}
235+
}
236+
237+
signer := deriveSigner(senderKey)
238+
239+
tx, err := NewSmartContractTransaction(
240+
contractName,
241+
codeBody,
242+
network.Version,
243+
network.ChainID,
244+
signer,
245+
0,
246+
0,
247+
stacks.AnchorModeOnChainOnly,
248+
stacks.PostConditionModeDeny,
249+
)
250+
if err != nil {
251+
return nil, &CustomError{Message: "Failed to create transaction", Err: err}
252+
}
253+
254+
err = createAndSignTransaction(tx, network, senderAddress, senderKey, fee, nonce)
255+
if err != nil {
256+
return nil, err
257+
}
258+
259+
return tx, nil
260+
}
261+
224262
func MakeContractCall(
225263
contractAddress string,
226264
contractName string,

‎pkg/transaction/builder_test.go

+110
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ import (
77
"testing"
88

99
"github.com/icon-project/stacks-go-sdk/pkg/clarity"
10+
"github.com/icon-project/stacks-go-sdk/pkg/crypto"
1011
"github.com/icon-project/stacks-go-sdk/pkg/stacks"
12+
"github.com/stretchr/testify/assert"
1113
)
1214

1315
func TestMakeSTXTokenTransfer(t *testing.T) {
@@ -74,3 +76,111 @@ func TestMakeSTXTokenTransfer(t *testing.T) {
7476
t.Errorf("Expected nonce %d, got %d", specifiedNonce.Uint64(), tx2.Auth.OriginAuth.Nonce)
7577
}
7678
}
79+
80+
func TestMakeContractDeploy(t *testing.T) {
81+
contractName := "test-contract"
82+
codeBody := `(define-data-var counter int 0)
83+
(define-public (increment)
84+
(ok (var-set counter (+ (var-get counter) 1))))
85+
`
86+
network := stacks.NewStacksTestnet()
87+
senderAddress := "ST15C893XJFJ6FSKM020P9JQDB5T7X6MQTXMBPAVH"
88+
senderKeyHex := "c1d5bb638aa70862621667f9997711fce692cad782694103f8d9561f62e9f19701"
89+
senderKey, _ := hex.DecodeString(senderKeyHex)
90+
91+
tests := []struct {
92+
name string
93+
contractName string
94+
codeBody string
95+
fee *big.Int
96+
nonce *big.Int
97+
expectedErr bool
98+
}{
99+
{
100+
name: "Valid contract with auto fee and nonce",
101+
contractName: contractName,
102+
codeBody: codeBody,
103+
fee: nil,
104+
nonce: nil,
105+
expectedErr: false,
106+
},
107+
{
108+
name: "Valid contract with specified fee and nonce",
109+
contractName: contractName,
110+
codeBody: codeBody,
111+
fee: big.NewInt(1000),
112+
nonce: big.NewInt(1),
113+
expectedErr: false,
114+
},
115+
{
116+
name: "Empty contract name",
117+
contractName: "",
118+
codeBody: codeBody,
119+
fee: nil,
120+
nonce: nil,
121+
expectedErr: true,
122+
},
123+
{
124+
name: "Empty code body",
125+
contractName: contractName,
126+
codeBody: "",
127+
fee: nil,
128+
nonce: nil,
129+
expectedErr: true,
130+
},
131+
}
132+
133+
for _, tt := range tests {
134+
t.Run(tt.name, func(t *testing.T) {
135+
tx, err := MakeContractDeploy(
136+
tt.contractName,
137+
tt.codeBody,
138+
*network,
139+
senderAddress,
140+
senderKey,
141+
tt.fee,
142+
tt.nonce,
143+
)
144+
145+
if tt.expectedErr {
146+
assert.Error(t, err)
147+
assert.Nil(t, tx)
148+
return
149+
}
150+
151+
assert.NoError(t, err)
152+
assert.NotNil(t, tx)
153+
154+
assert.Equal(t, tt.contractName, tx.Payload.ContractName)
155+
assert.Equal(t, tt.codeBody, tx.Payload.CodeBody)
156+
157+
if tt.fee != nil {
158+
assert.Equal(t, tt.fee.Uint64(), tx.Auth.OriginAuth.Fee)
159+
} else {
160+
assert.Greater(t, tx.Auth.OriginAuth.Fee, uint64(0), "Fee should be estimated")
161+
}
162+
163+
if tt.nonce != nil {
164+
assert.Equal(t, tt.nonce.Uint64(), tx.Auth.OriginAuth.Nonce)
165+
} else {
166+
assert.Greater(t, tx.Auth.OriginAuth.Nonce, uint64(0), "Nonce should be fetched")
167+
}
168+
169+
senderPublicKey := crypto.GetPublicKeyFromPrivate(senderKey)
170+
isValid, err := VerifyTransaction(tx, senderPublicKey)
171+
assert.NoError(t, err)
172+
assert.True(t, isValid)
173+
174+
serialized, err := tx.Serialize()
175+
assert.NoError(t, err)
176+
177+
deserialized, err := DeserializeTransaction(serialized)
178+
assert.NoError(t, err)
179+
180+
contractTx, ok := deserialized.(*SmartContractTransaction)
181+
assert.True(t, ok)
182+
assert.Equal(t, tx.Payload.ContractName, contractTx.Payload.ContractName)
183+
assert.Equal(t, tx.Payload.CodeBody, contractTx.Payload.CodeBody)
184+
})
185+
}
186+
}

‎pkg/transaction/payload.go

+112
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import (
44
"bytes"
55
"encoding/binary"
66
"errors"
7+
"fmt"
8+
"unicode"
79

810
"github.com/icon-project/stacks-go-sdk/internal/utils"
911
"github.com/icon-project/stacks-go-sdk/pkg/clarity"
@@ -21,6 +23,11 @@ type TokenTransferPayload struct {
2123
Memo string
2224
}
2325

26+
type SmartContractPayload struct {
27+
ContractName string
28+
CodeBody string
29+
}
30+
2431
type ContractCallPayload struct {
2532
ContractAddress clarity.ClarityValue // Can be either StandardPrincipal or ContractPrincipal
2633
ContractName string
@@ -32,6 +39,10 @@ func (t *TokenTransferTransaction) GetPayload() Payload {
3239
return &t.Payload
3340
}
3441

42+
func (t *SmartContractTransaction) GetPayload() Payload {
43+
return &t.Payload
44+
}
45+
3546
func (t *ContractCallTransaction) GetPayload() Payload {
3647
return &t.Payload
3748
}
@@ -49,6 +60,21 @@ func NewTokenTransferPayload(recipient string, amount uint64, memo string) (*Tok
4960
}, nil
5061
}
5162

63+
func NewSmartContractPayload(contractName string, codeBody string) (*SmartContractPayload, error) {
64+
if err := validateContractName(contractName); err != nil {
65+
return nil, err
66+
}
67+
68+
if err := validateCodeBody(codeBody); err != nil {
69+
return nil, err
70+
}
71+
72+
return &SmartContractPayload{
73+
ContractName: contractName,
74+
CodeBody: codeBody,
75+
}, nil
76+
}
77+
5278
func NewContractCallPayload(contractAddress string, contractName string, functionName string, functionArgs []clarity.ClarityValue) (*ContractCallPayload, error) {
5379
principalCV, err := clarity.StringToPrincipal(contractAddress)
5480
if err != nil {
@@ -114,6 +140,63 @@ func (p *TokenTransferPayload) Deserialize(data []byte) (int, error) {
114140
return offset, nil
115141
}
116142

143+
func (p *SmartContractPayload) Serialize() ([]byte, error) {
144+
buf := make([]byte, 0, len(p.ContractName)+len(p.CodeBody)+6) // 1 + 1 + 4 bytes for headers
145+
146+
// Payload type
147+
buf = append(buf, byte(stacks.PayloadTypeSmartContract))
148+
149+
// Contract name
150+
if len(p.ContractName) > stacks.MaxStringLengthBytes {
151+
return nil, fmt.Errorf("contract name exceeds maximum length of %d", stacks.MaxStringLengthBytes)
152+
}
153+
buf = append(buf, byte(len(p.ContractName)))
154+
buf = append(buf, []byte(p.ContractName)...)
155+
156+
// Code body with 4-byte length prefix
157+
codeBodyLen := make([]byte, 4)
158+
binary.BigEndian.PutUint32(codeBodyLen, uint32(len(p.CodeBody)))
159+
buf = append(buf, codeBodyLen...)
160+
buf = append(buf, []byte(p.CodeBody)...)
161+
162+
return buf, nil
163+
}
164+
165+
func (p *SmartContractPayload) Deserialize(data []byte) (int, error) {
166+
if len(data) < 2 || stacks.PayloadType(data[0]) != stacks.PayloadTypeSmartContract {
167+
return 0, errors.New("invalid smart contract payload")
168+
}
169+
170+
offset := 1
171+
172+
// Contract name
173+
nameLen := int(data[offset])
174+
offset++
175+
if nameLen > stacks.MaxStringLengthBytes {
176+
return 0, fmt.Errorf("contract name length %d exceeds maximum %d", nameLen, stacks.MaxStringLengthBytes)
177+
}
178+
if len(data[offset:]) < nameLen {
179+
return 0, errors.New("insufficient data for contract name")
180+
}
181+
p.ContractName = string(data[offset : offset+nameLen])
182+
offset += nameLen
183+
184+
// Code body
185+
if len(data[offset:]) < 4 {
186+
return 0, errors.New("insufficient data for code body length")
187+
}
188+
codeBodyLen := binary.BigEndian.Uint32(data[offset : offset+4])
189+
offset += 4
190+
191+
if len(data[offset:]) < int(codeBodyLen) {
192+
return 0, errors.New("insufficient data for code body")
193+
}
194+
p.CodeBody = string(data[offset : offset+int(codeBodyLen)])
195+
offset += int(codeBodyLen)
196+
197+
return offset, nil
198+
}
199+
117200
func (p *ContractCallPayload) Serialize() ([]byte, error) {
118201
buf := make([]byte, 0, 128)
119202

@@ -202,3 +285,32 @@ func (p *ContractCallPayload) Deserialize(data []byte) (int, error) {
202285

203286
return offset, nil
204287
}
288+
289+
func validateContractName(name string) error {
290+
if len(name) == 0 || len(name) > stacks.MaxStringLengthBytes {
291+
return fmt.Errorf("contract name must be between 1 and %d characters", stacks.MaxStringLengthBytes)
292+
}
293+
294+
// First character must be a letter
295+
if !unicode.IsLetter(rune(name[0])) {
296+
return errors.New("contract name must start with a letter")
297+
}
298+
299+
// Subsequent characters must be letters, numbers, hyphens, or underscores
300+
for _, r := range name[1:] {
301+
if !unicode.IsLetter(r) && !unicode.IsNumber(r) && r != '-' && r != '_' {
302+
return errors.New("contract name can only contain letters, numbers, hyphens, and underscores")
303+
}
304+
}
305+
306+
return nil
307+
}
308+
309+
func validateCodeBody(code string) error {
310+
for i, r := range code {
311+
if r != '\n' && r != '\t' && (r < 0x20 || r > 0x7e) {
312+
return fmt.Errorf("invalid character in code body at position %d: %q", i, r)
313+
}
314+
}
315+
return nil
316+
}

0 commit comments

Comments
 (0)
Please sign in to comment.