Skip to content

Commit b5e20f8

Browse files
authored
Merge pull request #18 from icon-project/feature/ci-hooks
ci: add GitHub Actions workflow
2 parents 26a4956 + c3a2c8a commit b5e20f8

File tree

10 files changed

+133
-27
lines changed

10 files changed

+133
-27
lines changed

.github/workflows/go.yml

+95
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
name: Go
2+
3+
on:
4+
push:
5+
branches: [ main ]
6+
pull_request:
7+
branches: [ main ]
8+
9+
jobs:
10+
11+
lint:
12+
runs-on: ubuntu-latest
13+
steps:
14+
- uses: actions/checkout@v3
15+
16+
- name: Set up Go
17+
uses: actions/setup-go@v3
18+
with:
19+
go-version: '1.23'
20+
21+
- name: golangci-lint
22+
uses: golangci/golangci-lint-action@v3
23+
with:
24+
version: latest
25+
26+
build:
27+
runs-on: ubuntu-latest
28+
needs: lint
29+
steps:
30+
- uses: actions/checkout@v3
31+
32+
- name: Set up Go
33+
uses: actions/setup-go@v3
34+
with:
35+
go-version: '1.23'
36+
37+
- name: Cache Go modules
38+
uses: actions/cache@v3
39+
with:
40+
path: ~/go/pkg/mod
41+
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
42+
restore-keys: |
43+
${{ runner.os }}-go-
44+
45+
- name: Build
46+
run: go build -v ./...
47+
48+
unit-tests:
49+
runs-on: ubuntu-latest
50+
needs: build
51+
steps:
52+
- uses: actions/checkout@v3
53+
54+
- name: Set up Go
55+
uses: actions/setup-go@v3
56+
with:
57+
go-version: '1.23'
58+
59+
- name: Cache Go modules
60+
uses: actions/cache@v3
61+
with:
62+
path: ~/go/pkg/mod
63+
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
64+
restore-keys: |
65+
${{ runner.os }}-go-
66+
67+
- name: Run Unit Tests with Coverage
68+
run: go test -v -coverprofile=coverage.txt ./pkg/...
69+
70+
- name: Upload results to Codecov
71+
uses: codecov/codecov-action@v4
72+
with:
73+
token: ${{ secrets.CODECOV_TOKEN }}
74+
75+
integration-tests:
76+
runs-on: ubuntu-latest
77+
needs: build
78+
steps:
79+
- uses: actions/checkout@v3
80+
81+
- name: Set up Go
82+
uses: actions/setup-go@v3
83+
with:
84+
go-version: '1.23'
85+
86+
- name: Cache Go modules
87+
uses: actions/cache@v3
88+
with:
89+
path: ~/go/pkg/mod
90+
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
91+
restore-keys: |
92+
${{ runner.os }}-go-
93+
94+
- name: Run Integration Tests
95+
run: go test -v ./test/integration/...

README.md

+2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
[![codecov](https://codecov.io/gh/icon-project/stacks-go-sdk/graph/badge.svg?token=CvLaGibygJ)](https://codecov.io/gh/icon-project/stacks-go-sdk)
2+
13
# Unofficial Stacks Blockchain SDK for Go
24

35
Send tokens and call Clarity smart contracts on the Stacks blockchain with Golang.

go.mod

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module github.com/icon-project/stacks-go-sdk
22

3-
go 1.22.5
3+
go 1.23
44

55
require (
66
github.com/btcsuite/btcd/btcec/v2 v2.3.4

pkg/crypto/signature.go

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"errors"
99
"fmt"
1010

11+
// nolint:staticcheck // SA1019: RIPEMD-160 is required for compatibility
1112
"golang.org/x/crypto/ripemd160"
1213

1314
"github.com/btcsuite/btcd/btcec/v2"

pkg/stacks/network.go

+4
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ func (n *StacksNetwork) GetTransactionFeeEstimateAPIURL() string {
4343
return fmt.Sprintf("%s/v2/fees/transaction", n.coreAPIURL)
4444
}
4545

46+
func (n *StacksNetwork) GetNonceAPIURL(address string) string {
47+
return fmt.Sprintf("%s/extended/v1/address/%s/nonces", n.coreAPIURL, address)
48+
}
49+
4650
func (n *StacksNetwork) FetchFn(url string) (*http.Response, error) {
4751
return http.Get(url)
4852
}

pkg/transaction/builder.go

+19-11
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,13 @@ func (e *CustomError) Error() string {
2727
return e.Message
2828
}
2929

30+
type NonceInfo struct {
31+
LastExecutedTxNonce *int64 `json:"last_executed_tx_nonce"`
32+
LastMempoolTxNonce *int64 `json:"last_mempool_tx_nonce"`
33+
PossibleNextNonce int64 `json:"possible_next_nonce"`
34+
DetectedMissingNonces []int64 `json:"detected_missing_nonces"`
35+
}
36+
3037
func makeRequest(url string, method string, payload interface{}) ([]byte, error) {
3138
client := resty.New()
3239
var resp *resty.Response
@@ -55,23 +62,25 @@ func makeRequest(url string, method string, payload interface{}) ([]byte, error)
5562
return resp.Body(), nil
5663
}
5764

58-
func getNonce(address string, network stacks.StacksNetwork) (*big.Int, error) {
59-
url := network.GetAccountAPIURL(address)
65+
func getNextNonce(address string, network stacks.StacksNetwork) (*big.Int, error) {
66+
url := network.GetNonceAPIURL(address)
67+
6068
body, err := makeRequest(url, "GET", nil)
6169
if err != nil {
62-
return nil, &CustomError{Message: "Error fetching nonce", Err: err}
70+
return nil, &CustomError{Message: "Error fetching nonce info", Err: err}
6371
}
6472

65-
var result struct {
66-
Nonce uint64 `json:"nonce"`
73+
var nonceInfo NonceInfo
74+
err = json.Unmarshal(body, &nonceInfo)
75+
if err != nil {
76+
return nil, &CustomError{Message: "Error parsing nonce info", Err: err}
6777
}
6878

69-
err = json.Unmarshal(body, &result)
70-
if err != nil {
71-
return nil, &CustomError{Message: "Error parsing JSON response", Err: err}
79+
if len(nonceInfo.DetectedMissingNonces) > 0 {
80+
fmt.Printf("Warning: Detected missing nonces: %v\n", nonceInfo.DetectedMissingNonces)
7281
}
7382

74-
return big.NewInt(int64(result.Nonce)), nil
83+
return big.NewInt(nonceInfo.PossibleNextNonce), nil
7584
}
7685

7786
func estimateTransactionFee(tx StacksTransaction, network stacks.StacksNetwork) (*big.Int, error) {
@@ -154,7 +163,7 @@ func createAndSignTransaction(tx StacksTransaction, network stacks.StacksNetwork
154163
}
155164

156165
if nonce == nil {
157-
nonce, err = getNonce(senderAddress, network)
166+
nonce, err = getNextNonce(senderAddress, network)
158167
if err != nil {
159168
return &CustomError{Message: "Failed to fetch nonce", Err: err}
160169
}
@@ -254,7 +263,6 @@ func BroadcastTransaction(tx StacksTransaction, network *stacks.StacksNetwork) (
254263
SetHeader("Content-Type", "application/octet-stream").
255264
SetBody(serializedTx).
256265
Post(url)
257-
258266
if err != nil {
259267
return "", &CustomError{Message: "Failed to send transaction", Err: err}
260268
}

pkg/transaction/builder_test.go

+1-3
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,11 @@ func TestMakeSTXTokenTransfer(t *testing.T) {
1515
amount := big.NewInt(1000000) // 1 STX
1616
memo := "Test transfer"
1717
network := stacks.NewStacksTestnet()
18-
senderAddress := "ST2CY5V39NHDPWSXMW9QDT3HC3GD6Q6XX4CFRK9AG"
18+
senderAddress := "ST15C893XJFJ6FSKM020P9JQDB5T7X6MQTXMBPAVH"
1919
senderKeyHex := "c1d5bb638aa70862621667f9997711fce692cad782694103f8d9561f62e9f19701"
2020
senderKey, _ := hex.DecodeString(senderKeyHex)
2121

2222
tx, err := MakeSTXTokenTransfer(recipient, *amount, memo, *network, senderAddress, senderKey, nil, nil)
23-
2423
if err != nil {
2524
t.Fatalf("makeSTXTokenTransfer failed: %v", err)
2625
}
@@ -63,7 +62,6 @@ func TestMakeSTXTokenTransfer(t *testing.T) {
6362
specifiedFee := big.NewInt(180)
6463
specifiedNonce := big.NewInt(3)
6564
tx2, err := MakeSTXTokenTransfer(recipient, *amount, memo, *network, senderAddress, senderKey, specifiedFee, specifiedNonce)
66-
6765
if err != nil {
6866
t.Fatalf("makeSTXTokenTransfer with specified fee and nonce failed: %v", err)
6967
}

pkg/transaction/transaction.go

+6-9
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,8 @@ func NewContractCallTransaction(
9696
nonce uint64,
9797
fee uint64,
9898
anchorMode stacks.AnchorMode,
99-
postConditionMode stacks.PostConditionMode) (*ContractCallTransaction, error) {
99+
postConditionMode stacks.PostConditionMode,
100+
) (*ContractCallTransaction, error) {
100101
payload, err := NewContractCallPayload(contractAddress, contractName, functionName, functionArgs)
101102
if err != nil {
102103
return nil, err
@@ -200,11 +201,10 @@ func (t *TokenTransferTransaction) Deserialize(data []byte) error {
200201
}
201202
offset++
202203

203-
payloadLen, err := t.Payload.Deserialize(data[offset:])
204+
_, err = t.Payload.Deserialize(data[offset:])
204205
if err != nil {
205206
return fmt.Errorf("failed to deserialize token transfer payload: %w", err)
206207
}
207-
offset += payloadLen
208208

209209
return nil
210210
}
@@ -286,11 +286,10 @@ func (t *ContractCallTransaction) Deserialize(data []byte) error {
286286
}
287287
offset++
288288

289-
payloadLen, err := t.Payload.Deserialize(data[offset:])
289+
_, err = t.Payload.Deserialize(data[offset:])
290290
if err != nil {
291291
return fmt.Errorf("failed to deserialize contract call payload: %w", err)
292292
}
293-
offset += payloadLen
294293

295294
return nil
296295
}
@@ -345,19 +344,17 @@ func DeserializeTransaction(data []byte) (StacksTransaction, error) {
345344
switch payloadType {
346345
case stacks.PayloadTypeTokenTransfer:
347346
tokenTx := &TokenTransferTransaction{BaseTransaction: baseTx}
348-
payloadLen, err := tokenTx.Payload.Deserialize(data[offset:])
347+
_, err := tokenTx.Payload.Deserialize(data[offset:])
349348
if err != nil {
350349
return nil, fmt.Errorf("failed to deserialize token transfer payload: %w", err)
351350
}
352-
offset += payloadLen
353351
tx = tokenTx
354352
case stacks.PayloadTypeContractCall:
355353
contractTx := &ContractCallTransaction{BaseTransaction: baseTx}
356-
payloadLen, err := contractTx.Payload.Deserialize(data[offset:])
354+
_, err := contractTx.Payload.Deserialize(data[offset:])
357355
if err != nil {
358356
return nil, fmt.Errorf("failed to deserialize contract call payload: %w", err)
359357
}
360-
offset += payloadLen
361358
tx = contractTx
362359
default:
363360
return nil, fmt.Errorf("unsupported payload type: %d", payloadType)

pkg/transaction/transaction_test.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -287,7 +287,8 @@ func assertTokenTransferTransactionFields(t *testing.T, tx *TokenTransferTransac
287287
fee uint64
288288
anchorMode stacks.AnchorMode
289289
postConditionMode stacks.PostConditionMode
290-
}) {
290+
},
291+
) {
291292
assert.Equal(t, expected.version, tx.Version)
292293
assert.Equal(t, expected.chainID, tx.ChainID)
293294
assert.Equal(t, expected.signer, tx.Auth.OriginAuth.Signer)

test/integration/transaction_test.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ func TestBroadcastSTXTokenTransferTransaction(t *testing.T) {
1919
t.Fatalf("Failed to derive private key: %v", err)
2020
}
2121

22-
senderAddress := "ST2CY5V39NHDPWSXMW9QDT3HC3GD6Q6XX4CFRK9AG"
22+
senderAddress := "ST15C893XJFJ6FSKM020P9JQDB5T7X6MQTXMBPAVH"
2323
senderPublicKey := crypto.GetPublicKeyFromPrivate(privateKey)
2424
var signerArray [20]byte
2525
copy(signerArray[:], crypto.Hash160(senderPublicKey))
@@ -53,7 +53,7 @@ func TestBroadcastContractCallTransaction(t *testing.T) {
5353
t.Fatalf("Failed to derive private key: %v", err)
5454
}
5555

56-
senderAddress := "ST2CY5V39NHDPWSXMW9QDT3HC3GD6Q6XX4CFRK9AG"
56+
senderAddress := "ST15C893XJFJ6FSKM020P9JQDB5T7X6MQTXMBPAVH"
5757
senderPublicKey := crypto.GetPublicKeyFromPrivate(privateKey)
5858
var signerArray [20]byte
5959
copy(signerArray[:], crypto.Hash160(senderPublicKey))

0 commit comments

Comments
 (0)