|
| 1 | +package timelock |
| 2 | + |
| 3 | +import ( |
| 4 | + "context" |
| 5 | + "encoding/json" |
| 6 | + "fmt" |
| 7 | + |
| 8 | + "github.com/gagliardetto/solana-go" |
| 9 | + "github.com/gagliardetto/solana-go/rpc" |
| 10 | + "github.com/samber/lo" |
| 11 | + mcmssolanasdk "github.com/smartcontractkit/mcms/sdk/solana" |
| 12 | + mcmstypes "github.com/smartcontractkit/mcms/types" |
| 13 | + |
| 14 | + timelockbindings "github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings/timelock" |
| 15 | + solanautils "github.com/smartcontractkit/chainlink-ccip/chains/solana/utils/common" |
| 16 | +) |
| 17 | + |
| 18 | +// execute runs the CallScheduled operation if: |
| 19 | +// - The predecessor operation is finished |
| 20 | +// - The operation is ready to be executed |
| 21 | +// Otherwise the operation will throw an info log and wait for a future tick. |
| 22 | +func (tw *WorkerSolana) execute(ctx context.Context, op []TimelockCallScheduled) { |
| 23 | + if len(op) == 0 { |
| 24 | + tw.logger.Warn("no calls given") |
| 25 | + return |
| 26 | + } |
| 27 | + opId := op[0].Id() |
| 28 | + |
| 29 | + isReady, err := tw.inspector.IsOperationReady(ctx, tw.timelockAddress, opId) |
| 30 | + if err != nil { |
| 31 | + tw.logger.Errorf("unable to read operation %x \"ready\" status: %s", opId, err.Error()) |
| 32 | + return |
| 33 | + } |
| 34 | + if !isReady { |
| 35 | + tw.logger.Infof("skipping operation %x: not ready", opId) |
| 36 | + return |
| 37 | + } |
| 38 | + |
| 39 | + mcmsTransactions, err := tw.mapCallScheduledEventsToMcmsTransactions(ctx, op) |
| 40 | + if err != nil { |
| 41 | + tw.logger.Errorf("unable to convert call scheduled events to mcms transactions: %v", err) |
| 42 | + return |
| 43 | + } |
| 44 | + batchOp := mcmstypes.BatchOperation{Transactions: mcmsTransactions} |
| 45 | + |
| 46 | + tw.logger.Debugf("execute operation %x", opId) |
| 47 | + timelockExecutor := mcmssolanasdk.NewTimelockExecutor(tw.solanaClient, tw.privateKey) |
| 48 | + result, err := timelockExecutor.Execute(ctx, batchOp, tw.timelockAddress, op[0].Predecessor(), op[0].Salt()) |
| 49 | + if err != nil { |
| 50 | + tw.logger.Errorf("execute operation %x error: %s", opId, err.Error()) |
| 51 | + return |
| 52 | + } |
| 53 | + |
| 54 | + tw.logger.Infof("execute operation %x success: %s", opId, result.Hash) |
| 55 | +} |
| 56 | + |
| 57 | +func (tw *WorkerSolana) mapCallScheduledEventsToMcmsTransactions( |
| 58 | + ctx context.Context, events []TimelockCallScheduled, |
| 59 | +) ([]mcmstypes.Transaction, error) { |
| 60 | + transactions := make([]mcmstypes.Transaction, len(events)) |
| 61 | + for i, event := range events { |
| 62 | + solanaEvent := event.(*solanaTimelockCallScheduled).callScheduledEvent |
| 63 | + |
| 64 | + accounts, err := tw.getAccountsFromOperation(ctx, solanaEvent.ID) |
| 65 | + if err != nil { |
| 66 | + return nil, fmt.Errorf("failed to get accounts from operation pda: %w", err) |
| 67 | + } |
| 68 | + |
| 69 | + additionalFields := mcmssolanasdk.AdditionalFields{ |
| 70 | + Accounts: accounts, |
| 71 | + Value: nil, // REVIEW |
| 72 | + } |
| 73 | + additionalFieldsJson, err := json.Marshal(additionalFields) |
| 74 | + if err != nil { |
| 75 | + return nil, fmt.Errorf("failed to marshsal additional fields: %w", err) |
| 76 | + } |
| 77 | + |
| 78 | + transactions[i] = mcmstypes.Transaction{ |
| 79 | + To: solanaEvent.Target.String(), |
| 80 | + Data: solanaEvent.Data, |
| 81 | + AdditionalFields: additionalFieldsJson, |
| 82 | + } |
| 83 | + } |
| 84 | + return transactions, nil |
| 85 | +} |
| 86 | + |
| 87 | +func (tw *WorkerSolana) getAccountsFromOperation(ctx context.Context, operationID operationKey) ([]*solana.AccountMeta, error) { |
| 88 | + _, pdaSeed, err := mcmssolanasdk.ParseContractAddress(tw.timelockAddress) |
| 89 | + if err != nil { |
| 90 | + return nil, fmt.Errorf("failed to parse timelock address: %w", err) |
| 91 | + } |
| 92 | + |
| 93 | + operationPDA, err := FindOperationPDA(tw.timelockProgramKey, pdaSeed, operationID) |
| 94 | + if err != nil { |
| 95 | + return nil, fmt.Errorf("failed to find operation PDA: %w", err) |
| 96 | + } |
| 97 | + |
| 98 | + var scheduledOperation timelockbindings.Operation |
| 99 | + err = solanautils.GetAccountDataBorshInto(ctx, tw.solanaClient, operationPDA, rpc.CommitmentConfirmed, &scheduledOperation) |
| 100 | + if err != nil { |
| 101 | + return nil, fmt.Errorf("failed to get operation data from PDA account: %w", err) |
| 102 | + } |
| 103 | + tw.logger.Debugf("scheduledOpAccount: %+v", scheduledOperation) |
| 104 | + |
| 105 | + accountMap := make(map[string]*solana.AccountMeta) |
| 106 | + for _, instruction := range scheduledOperation.Instructions { |
| 107 | + // add program ID of the instruction as an account(CPI) |
| 108 | + progKey := instruction.ProgramId.String() |
| 109 | + _, exists := accountMap[progKey] |
| 110 | + if !exists { |
| 111 | + accountMap[progKey] = &solana.AccountMeta{ |
| 112 | + PublicKey: instruction.ProgramId, |
| 113 | + IsSigner: false, |
| 114 | + IsWritable: false, // program accounts are never writable |
| 115 | + } |
| 116 | + } |
| 117 | + |
| 118 | + // all other accounts from the instruction |
| 119 | + for _, account := range instruction.Accounts { |
| 120 | + key := account.Pubkey.String() |
| 121 | + existingAccount, exists := accountMap[key] |
| 122 | + if exists { |
| 123 | + existingAccount.IsWritable = existingAccount.IsWritable || account.IsWritable |
| 124 | + accountMap[key] = existingAccount |
| 125 | + } else { |
| 126 | + accountMap[key] = &solana.AccountMeta{ |
| 127 | + PublicKey: account.Pubkey, |
| 128 | + IsSigner: false, // force false for CPI |
| 129 | + IsWritable: account.IsWritable, |
| 130 | + } |
| 131 | + } |
| 132 | + } |
| 133 | + } |
| 134 | + |
| 135 | + return lo.Values(accountMap), nil |
| 136 | + |
| 137 | + // // todo: debugging, remove after PoC |
| 138 | + // t.Log(len(remainingAccounts), "remaining accounts") |
| 139 | + // t.Log(len(op1.RemainingAccounts()), "original remaining accounts") |
| 140 | + // |
| 141 | + // t.Log("--- NEW IMPLEMENTATION ACCOUNTS ---") |
| 142 | + // for i, acc := range remainingAccounts { |
| 143 | + // t.Logf("Account %d: PubKey=%s, IsSigner=%t, IsWritable=%t", |
| 144 | + // i, acc.PublicKey.String(), acc.IsSigner, acc.IsWritable) |
| 145 | + // } |
| 146 | + // |
| 147 | + // t.Log("--- ORIGINAL IMPLEMENTATION ACCOUNTS ---") |
| 148 | + // for i, acc := range op1.RemainingAccounts() { |
| 149 | + // t.Logf("Account %d: PubKey=%s, IsSigner=%t, IsWritable=%t", |
| 150 | + // i, acc.PublicKey.String(), acc.IsSigner, acc.IsWritable) |
| 151 | + // } |
| 152 | + // |
| 153 | + // t.Log("--- CHECKING FOR MISSING ACCOUNTS ---") |
| 154 | + // originalMap := make(map[string]bool) |
| 155 | + // for _, acc := range op1.RemainingAccounts() { |
| 156 | + // originalMap[acc.PublicKey.String()] = true |
| 157 | + // } |
| 158 | + // |
| 159 | + // for _, acc := range remainingAccounts { |
| 160 | + // if _, exists := originalMap[acc.PublicKey.String()]; !exists { |
| 161 | + // t.Logf("WARNING: Account in new implementation NOT found in original: %s", |
| 162 | + // acc.PublicKey.String()) |
| 163 | + // } |
| 164 | + // } |
| 165 | + |
| 166 | + // // Check for extra accounts |
| 167 | + // newMap := make(map[string]bool) |
| 168 | + // for _, acc := range remainingAccounts { |
| 169 | + // newMap[acc.PublicKey.String()] = true |
| 170 | + // } |
| 171 | + // |
| 172 | + // for _, acc := range op1.RemainingAccounts() { |
| 173 | + // if _, exists := newMap[acc.PublicKey.String()]; !exists { |
| 174 | + // t.Logf("WARNING: Account in original implementation NOT found in new: %s", |
| 175 | + // acc.PublicKey.String()) |
| 176 | + // } |
| 177 | + // } |
| 178 | +} |
| 179 | + |
| 180 | +func FindOperationPDA( |
| 181 | + programID solana.PublicKey, timelockID mcmssolanasdk.PDASeed, opID [32]byte, |
| 182 | +) (solana.PublicKey, error) { |
| 183 | + seeds := [][]byte{[]byte("timelock_operation"), timelockID[:], opID[:]} |
| 184 | + return findPDA(programID, seeds) |
| 185 | +} |
| 186 | + |
| 187 | +func findPDA(programID solana.PublicKey, seeds [][]byte) (solana.PublicKey, error) { |
| 188 | + pda, _, err := solana.FindProgramAddress(seeds, programID) |
| 189 | + if err != nil { |
| 190 | + return solana.PublicKey{}, fmt.Errorf("unable to find %s pda: %w", string(seeds[0]), err) |
| 191 | + } |
| 192 | + |
| 193 | + return pda, nil |
| 194 | +} |
0 commit comments