Skip to content
13 changes: 13 additions & 0 deletions apps/solana/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"github.com/blocto/solana-go-sdk/types"
"github.com/gagliardetto/solana-go"
tokenAta "github.com/gagliardetto/solana-go/programs/associated-token-account"
"github.com/gagliardetto/solana-go/programs/memo"
"github.com/gagliardetto/solana-go/programs/system"
"github.com/gagliardetto/solana-go/programs/token"
)
Expand Down Expand Up @@ -50,13 +51,13 @@
CreatedAt time.Time

Uri string
Asset *bot.AssetNetwork

Check failure on line 54 in apps/solana/common.go

View workflow job for this annotation

GitHub Actions / lint

undefined: bot (typecheck)
PrivateKey *solana.PrivateKey

Check failure on line 55 in apps/solana/common.go

View workflow job for this annotation

GitHub Actions / lint

undefined: solana (typecheck)
}

type NonceAccount struct {
Address solana.PublicKey

Check failure on line 59 in apps/solana/common.go

View workflow job for this annotation

GitHub Actions / lint

undefined: solana (typecheck)
Hash solana.Hash

Check failure on line 60 in apps/solana/common.go

View workflow job for this annotation

GitHub Actions / lint

undefined: solana (typecheck)
}

type TokenTransfer struct {
Expand Down Expand Up @@ -383,6 +384,18 @@
return nil, false
}

func DecodeMemo(accounts solana.AccountMetaSlice, data []byte) (*memo.Create, error) {
ix, err := memo.DecodeInstruction(accounts, data)
if err != nil {
return nil, err
}
memo, ok := ix.Impl.(*memo.Create)
if ok {
return memo, nil
}
return nil, fmt.Errorf("invalid memo instruction")
}

func DecodeNonceAdvance(accounts solana.AccountMetaSlice, data []byte) (*system.AdvanceNonceAccount, error) {
ix, err := system.DecodeInstruction(accounts, data)
if err != nil {
Expand Down
40 changes: 39 additions & 1 deletion apps/solana/transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/gagliardetto/solana-go"
tokenAta "github.com/gagliardetto/solana-go/programs/associated-token-account"
computebudget "github.com/gagliardetto/solana-go/programs/compute-budget"
"github.com/gagliardetto/solana-go/programs/memo"
"github.com/gagliardetto/solana-go/programs/system"
"github.com/gagliardetto/solana-go/programs/token"
"github.com/gagliardetto/solana-go/rpc"
Expand Down Expand Up @@ -194,7 +195,7 @@ func (c *Client) CreateMints(ctx context.Context, payer, mtg solana.PublicKey, a
return tx, nil
}

func (c *Client) TransferOrMintTokens(ctx context.Context, payer, mtg solana.PublicKey, nonce NonceAccount, transfers []*TokenTransfer) (*solana.Transaction, error) {
func (c *Client) TransferOrMintTokens(ctx context.Context, payer, mtg solana.PublicKey, nonce NonceAccount, transfers []*TokenTransfer, memoStr string) (*solana.Transaction, error) {
builder := c.buildInitialTxWithNonceAccount(ctx, payer, nonce)

for _, transfer := range transfers {
Expand Down Expand Up @@ -234,6 +235,15 @@ func (c *Client) TransferOrMintTokens(ctx context.Context, payer, mtg solana.Pub
)
}

if memoStr != "" {
builder.AddInstruction(
memo.NewMemoInstruction(
[]byte(memoStr),
payer,
).Build(),
)
}

tx, err := builder.Build()
if err != nil {
panic(err)
Expand Down Expand Up @@ -461,6 +471,34 @@ func ExtractMintsFromTransaction(tx *solana.Transaction) []string {
return assets
}

func ExtractMemoFromTransaction(ctx context.Context, tx *solana.Transaction, meta *rpc.TransactionMeta, payer solana.PublicKey) string {
if meta.Err != nil {
panic(fmt.Sprint(meta.Err))
}

msg := tx.Message
for _, ins := range msg.Instructions {
programKey, err := msg.Program(ins.ProgramIDIndex)
if err != nil {
panic(err)
}
if !programKey.Equals(solana.MemoProgramID) {
continue
}
accounts, err := ins.ResolveInstructionAccounts(&tx.Message)
if err != nil {
panic(err)
}
if memo, err := DecodeMemo(accounts, ins.Data); err == nil {
if memo.GetSigner().PublicKey.String() == payer.String() {
return string(memo.Message)
}
}
}

return ""
}

func GetSignatureIndexOfAccount(tx solana.Transaction, publicKey solana.PublicKey) (int, error) {
index, err := tx.GetAccountIndex(publicKey)
if err == nil {
Expand Down
85 changes: 75 additions & 10 deletions solana/mvm.go
Original file line number Diff line number Diff line change
Expand Up @@ -550,7 +550,7 @@ func (node *Node) processConfirmCall(ctx context.Context, req *store.Request) ([
return nil, ""
case FlagConfirmCallFail:
callId := uuid.Must(uuid.FromBytes(extra[:16])).String()
call, err := node.store.ReadSystemCallByRequestId(ctx, callId, common.RequestStatePending)
call, err := node.store.ReadSystemCallByRequestId(ctx, callId, 0)
logger.Printf("store.ReadSystemCallByRequestId(%s) => %v %v", callId, call, err)
if err != nil {
panic(err)
Expand Down Expand Up @@ -697,6 +697,8 @@ func (node *Node) processDeposit(ctx context.Context, out *mtg.Action) ([]*mtg.T
}

var ts []*solanaApp.Transfer
var tx *solana.Transaction
var meta *rpc.TransactionMeta
if common.CheckTestEnvironment(ctx) {
ts = append(ts, &solanaApp.Transfer{
AssetId: out.AssetId,
Expand All @@ -712,10 +714,11 @@ func (node *Node) processDeposit(ctx context.Context, out *mtg.Action) ([]*mtg.T
if err != nil {
panic(err)
}
tx, err := rpcTx.Transaction.GetTransaction()
tx, err = rpcTx.Transaction.GetTransaction()
if err != nil {
panic(err)
}
meta = rpcTx.Meta
err = node.processTransactionWithAddressLookups(ctx, tx)
if err != nil {
panic(err)
Expand All @@ -737,11 +740,31 @@ func (node *Node) processDeposit(ctx context.Context, out *mtg.Action) ([]*mtg.T
continue
}
user, err := node.store.ReadUserByChainAddress(ctx, t.Sender)
logger.Verbosef("store.ReadUserByAddress(%s) => %v %v", t.Sender, user, err)
logger.Printf("store.ReadUserByAddress(%s) => %v %v", t.Sender, user, err)
if err != nil {
panic(err)
} else if user == nil {
continue
memo := solanaApp.ExtractMemoFromTransaction(ctx, tx, meta, node.SolanaPayer())
logger.Printf("solana.ExtractMemoFromTransaction(%s) => %s", tx.Signatures[0].String(), memo)
if memo == "" {
continue
}
call, err := node.store.ReadSystemCallByRequestId(ctx, memo, common.RequestStateFailed)
logger.Printf("store.ReadSystemCallByRequestId(%s) => %v %v", memo, call, err)
if err != nil {
panic(err)
}
if call == nil || call.Type != store.CallTypePrepare {
continue
}
superir, err := node.store.ReadSystemCallByRequestId(ctx, call.Superior, common.RequestStateFailed)
if err != nil {
panic(err)
}
user, err = node.store.ReadUser(ctx, superir.UserIdFromPublicPath())
if err != nil {
panic(err)
}
}
mix, err := bot.NewMixAddressFromString(user.MixAddress)
if err != nil {
Expand Down Expand Up @@ -791,6 +814,12 @@ func (node *Node) refundAndFailRequest(ctx context.Context, req *store.Request,
}

func (node *Node) failSystemCall(ctx context.Context, req *store.Request, call *store.SystemCall) ([]*mtg.Transaction, string) {
switch call.State {
case common.RequestStatePending, common.RequestStateFailed:
default:
return node.failRequest(ctx, req, "")
}

extra := req.ExtraBytes()
flag, extra := extra[0], extra[1:]

Expand All @@ -803,19 +832,29 @@ func (node *Node) failSystemCall(ctx context.Context, req *store.Request, call *
}

var outputs []*store.UserOutput
var mix *bot.MixAddress
switch call.Type {
case store.CallTypeMain, store.CallTypePrepare:
main := call
if call.Type == store.CallTypePrepare {
c, err := node.store.ReadSystemCallByRequestId(ctx, call.Superior, common.RequestStatePending)
logger.Printf("store.ReadSystemCallByRequestId(%s) => %v %v", call.Superior, call, err)
c, err := node.store.ReadSystemCallByRequestId(ctx, call.Superior, 0)
logger.Printf("store.ReadSystemCallByRequestId(%s) => %v %v", call.Superior, c, err)
if err != nil || c == nil {
panic(err)
}
main = c

user, err := node.store.ReadUser(ctx, main.UserIdFromPublicPath())
if err != nil {
panic(err)
}
mix, err = bot.NewMixAddressFromString(user.MixAddress)
if err != nil {
panic(err)
}
}

os, _, err := node.GetSystemCallReferenceOutputs(ctx, main.UserIdFromPublicPath(), main.RequestHash, common.RequestStatePending)
os, _, err := node.GetSystemCallReferenceOutputs(ctx, main.UserIdFromPublicPath(), main.RequestHash, 0)
if err != nil {
panic(err)
}
Expand All @@ -842,11 +881,29 @@ func (node *Node) failSystemCall(ctx context.Context, req *store.Request, call *
}
}

err = node.store.FailSystemCallWithRequest(ctx, req, call, post, session, outputs)
// refund external assets when prepare call failed
// solana assets would be transfered to user when mtg receives deposit
var txs []*mtg.Transaction
var compaction string
if call.Type == store.CallTypePrepare && mix != nil {
as := node.GetSystemCallRelatedAsset(ctx, outputs)
var assets []*ReferencedTxAsset
for _, a := range as {
if a.Solana {
continue
}
assets = append(assets, a)
}
if len(assets) > 0 {
txs, compaction = node.buildRefundTxs(ctx, req, call.RequestId, assets, mix.Members(), int(mix.Threshold))
}
}

err = node.store.FailSystemCallWithRequest(ctx, req, call, post, session, outputs, txs, compaction)
if err != nil {
panic(err)
}
return nil, ""
return txs, compaction
}

func (node *Node) checkConfirmCallSignature(ctx context.Context, signature string) (*store.SystemCall, *rpc.GetTransactionResult, error) {
Expand Down Expand Up @@ -897,7 +954,15 @@ func (node *Node) checkConfirmCallSignature(ctx context.Context, signature strin
}

func (node *Node) confirmBurnRelatedSystemCall(ctx context.Context, req *store.Request, call *store.SystemCall, rpcTx *rpc.GetTransactionResult, signature string) ([]*mtg.Transaction, string) {
user, err := node.store.ReadUser(ctx, call.UserIdFromPublicPath())
main := call
if call.Superior != call.RequestId {
c, err := node.store.ReadSystemCallByRequestId(ctx, call.Superior, 0)
if err != nil {
panic(err)
}
main = c
}
user, err := node.store.ReadUser(ctx, main.UserIdFromPublicPath())
if err != nil {
panic(err)
}
Expand Down
41 changes: 27 additions & 14 deletions solana/observer.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ func (node *Node) bootObserver(ctx context.Context, version string) {
panic(err)
}

err = node.refundFailedPrepareCalls(ctx)
if err != nil {
panic(err)
}

err = node.checkNonceAccounts(ctx)
if err != nil {
panic(err)
Expand Down Expand Up @@ -916,24 +921,32 @@ func (node *Node) processSuccessedCall(ctx context.Context, call *store.SystemCa
func (node *Node) processFailedCall(ctx context.Context, call *store.SystemCall, callError error) error {
logger.Printf("node.processFailedCall(%s)", call.RequestId)
id := common.UniqueId(call.RequestId, "confirm-fail")
cid := common.UniqueId(id, "post-process")
nonce := node.ReadSpareNonceAccountWithCall(ctx, cid)
extra := []byte{FlagConfirmCallFail}
extra = append(extra, uuid.Must(uuid.FromString(call.RequestId)).Bytes()...)

if call.Type == store.CallTypeMain {
cid := common.UniqueId(id, "post-process")
nonce := node.ReadSpareNonceAccountWithCall(ctx, cid)
tx := node.CreatePostProcessTransaction(ctx, call, nonce, nil, nil)
if tx != nil {
err := node.OccupyNonceAccountByCall(ctx, nonce, cid)
if err != nil {
return err
}
data, err := tx.MarshalBinary()
if err != nil {
panic(err)
}
extra = attachSystemCall(extra, cid, data)
var tx *solana.Transaction
switch call.Type {
case store.CallTypeMain:
tx = node.CreatePostProcessTransaction(ctx, call, nonce, nil, nil)
case store.CallTypePrepare:
superior, err := node.store.ReadSystemCallByRequestId(ctx, call.Superior, 0)
if err != nil {
panic(err)
}
tx = node.CreateRefundWithdrawalTransaction(ctx, call, superior, nonce)
}
if tx != nil {
err := node.OccupyNonceAccountByCall(ctx, nonce, cid)
if err != nil {
return err
}
data, err := tx.MarshalBinary()
if err != nil {
panic(err)
}
extra = attachSystemCall(extra, cid, data)
}

err := node.store.WriteFailedCallIfNotExist(ctx, call, callError.Error())
Expand Down
80 changes: 80 additions & 0 deletions solana/refund.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package solana

import (
"context"
"slices"
"strings"
"time"

"github.com/MixinNetwork/computer/store"
"github.com/MixinNetwork/mixin/logger"
"github.com/MixinNetwork/safe/common"
"github.com/gofrs/uuid/v5"
)

func (node *Node) processFailedPrepareCall(ctx context.Context, call *store.SystemCall) error {
logger.Printf("node.processFailedPrepareCall(%s)", call.RequestId)
id := common.UniqueId(call.RequestId, "confirm-prepare-fail")
cid := common.UniqueId(id, "post-process")
nonce := node.ReadSpareNonceAccountWithCall(ctx, cid)
extra := []byte{FlagConfirmCallFail}
extra = append(extra, uuid.Must(uuid.FromString(call.RequestId)).Bytes()...)

superior, err := node.store.ReadSystemCallByRequestId(ctx, call.Superior, 0)
if err != nil {
panic(err)
}
tx := node.CreateRefundWithdrawalTransaction(ctx, call, superior, nonce)
if tx != nil {
err := node.OccupyNonceAccountByCall(ctx, nonce, cid)
if err != nil {
return err
}
data, err := tx.MarshalBinary()
if err != nil {
panic(err)
}
extra = attachSystemCall(extra, cid, data)
}

return node.sendObserverTransactionToGroup(ctx, &common.Operation{
Id: id,
Type: OperationTypeConfirmCall,
Extra: extra,
}, nil)
}

func (node *Node) refundFailedPrepareCalls(ctx context.Context) error {
failedPrepareIds := []string{
"ccbb4fc6-737c-3428-a7b0-78d1de6ccbdd",
"933b85e2-2863-3253-99b7-2b5e6a512d32",
"eca70deb-8822-372e-a63b-f84a7cbaa129",
"a83cee37-f113-31e8-9c95-2e3490263c87",
"f9d47737-ca6e-3038-9307-b28c7ce59df7",
"5dea077c-ec74-3d92-a6f7-6fafc5f93697",
"39891351-d37f-322c-9fd8-b049cc629fe2",
}

key := "REFUND:FAILED:PREPARE:" + strings.Join(failedPrepareIds, ",")
val, err := node.store.ReadProperty(ctx, key)
if err != nil || val != "" {
return err
}

calls, err := node.store.ListFailedPrepareCalls(ctx)
if err != nil {
return err
}
for _, prepare := range calls {
if !slices.Contains(failedPrepareIds, prepare.RequestId) {
continue
}
err = node.processFailedPrepareCall(ctx, prepare)
if err != nil {
return err
}
time.Sleep(time.Second)
}

return node.store.WriteProperty(ctx, key, "refunded")
}
Loading
Loading