Skip to content

Commit aaec324

Browse files
authored
Merge pull request #84 from xnoname79/main
support parse squad-multiig transaction
2 parents ba6c3db + c7c691b commit aaec324

2 files changed

Lines changed: 128 additions & 2 deletions

File tree

internal/indexer/solana.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -385,7 +385,7 @@ func solanaParseTokenTransfer(ix solana.Instruction, accountKeys []solana.Accoun
385385
case uint64:
386386
amt = v
387387
}
388-
case "transferChecked":
388+
case "transferChecked", "transferCheckedWithFee":
389389
// tokenAmount: { amount:"..", decimals:n, uiAmountString:".." }
390390
if ta, _ := info["tokenAmount"].(map[string]any); ta != nil {
391391
switch v := ta["amount"].(type) {
@@ -444,7 +444,7 @@ func solanaParseTokenTransfer(ix solana.Instruction, accountKeys []solana.Accoun
444444
return "", "", "", 0, false
445445
}
446446
return accountKeys[srcIdx].Pubkey, accountKeys[dstIdx].Pubkey, "", amt, true
447-
case 12:
447+
case 12, 26: // 12 = transferChecked, 26 = transferCheckedWithFee (same account layout)
448448
if len(accIdx) < 3 || len(data) < 9 {
449449
return "", "", "", 0, false
450450
}

internal/indexer/solana_test.go

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,3 +244,129 @@ func TestParseSPLTransferChecked(t *testing.T) {
244244
tokenTransfer.FromAddress, tokenTransfer.ToAddress,
245245
tokenTransfer.Amount, tokenTransfer.AssetAddress, tokenTransfer.TransferIndex)
246246
}
247+
248+
// TestParseBatchSOLTransfer tests parsing of a batch transfer transaction containing
249+
// 20 native SOL transfers (1 lamport each) from a single sender to 20 different recipients.
250+
// Transaction: 5dKRr7cF6pNiuhKYY8RDF5rxTjbFWFGAzKgBXoYrPcg2uMx14uwfopiX1XiyZuu3aihMMRqzSSX9jQcWnamqw5DK
251+
func TestParseBatchSOLTransfer(t *testing.T) {
252+
if testing.Short() {
253+
t.Skip("skipping integration test")
254+
}
255+
256+
ctx := context.Background()
257+
c := newTestSolanaClient()
258+
259+
txSig := "5dKRr7cF6pNiuhKYY8RDF5rxTjbFWFGAzKgBXoYrPcg2uMx14uwfopiX1XiyZuu3aihMMRqzSSX9jQcWnamqw5DK"
260+
txResult, err := c.GetTransaction(ctx, txSig)
261+
require.NoError(t, err)
262+
require.NotNil(t, txResult, "transaction should exist on mainnet")
263+
264+
idx := newTestSolanaIndexer()
265+
block := txToBlockResult(txResult)
266+
267+
ts := uint64(0)
268+
if txResult.BlockTime != nil {
269+
ts = uint64(*txResult.BlockTime)
270+
}
271+
transfers := idx.extractSolanaTransfers("solana-mainnet", txResult.Slot, ts, block)
272+
273+
// This transaction contains 20 native SOL transfers of 1 lamport each
274+
expectedRecipients := []string{
275+
"5P1E2RDvWWsQYtHgC9Ng3SejPq7tLdk697tNWfkxwx5g",
276+
"JCxSuQwRa2Qbtxjrv9Csf2B8AtCMiLjfr24Bb27yheFL",
277+
"4Y84ZayuVutuW2e1Lq8qfq5JYwYkTn8CtutmZs3jW8oM",
278+
"9rRE1bEPcQxf3LSJphSd1jHbDVi4dLYWgY9g61YTCHdW",
279+
"3dUBc9cLiF92uWCdZVGmB7iKPTHVioeE5UqA8PEjUYXs",
280+
"FeAuJTC8wZypEuTQteys9PRdBPAL3ik8VuViDtQNm2tb",
281+
"BMvWVyMFoDhfwvqDG4q2uGmnP4sr4FhqT1hDSPbrbGRX",
282+
"3KSTPVNHmWvkwy3JcuKQH8sLogv1Gf9rauj8Z1TpPdbG",
283+
"DHY4ANQod7Ff67ojZPgzWePwHNSBeJwbWmye2SwR6F3V",
284+
"B9eWxGfXGsLhMPx65mdFiSayhqVAxWG4TGzaFpuTWQmH",
285+
"Avr7d3kwUcV8cUhzDmpbVjzPDCni9FSJYyNaDjuFPLZt",
286+
"HPDN6Ro6gPgYbNgnv7yhFCpyY3uYzT18epFxRFNZ985w",
287+
"EYidortrJzJGkR3r2gFg2sJVM6WJQNE7yZdzvhndGPVQ",
288+
"E4oy1PetbUo7UKww5Dmy9gHyNgrtQWPsC5DHBsVu3KQo",
289+
"H6EJh7pyygR8ghgPxe1j4NE3FC7BuzC4S2Cybw5fJMcg",
290+
"5E81HKzq7vXPgGcH3ZndGW56DtLBPC9rJD5RmAMdbE3Y",
291+
"H1PQRtvSEH2HwwtZUnBfxEerwbhPHhUvTCtDF66NSCY4",
292+
"9mSFaSJgQotHmL14xSMcRgNC9hnidNfGkyJ2fhvizKuj",
293+
"GLvb7P4q7AkX1j7r4S2fr4uoLWqDLWRsPkJqMMcNR9CE",
294+
"5XFz3x79Hi8zzQb8RuzNqzoqXkyxRCgDyvjK7BrfCKML",
295+
}
296+
297+
const expectedSender = "AZWibhw2cmpVFt4b45XGhiB8vSmYc2LfMGyHE3Lb97cM"
298+
299+
// All transfers should be native
300+
var nativeTransfers []types.Transaction
301+
for _, tx := range transfers {
302+
if tx.Type == constant.TxTypeNativeTransfer {
303+
nativeTransfers = append(nativeTransfers, tx)
304+
}
305+
}
306+
307+
require.Len(t, nativeTransfers, 20, "should detect all 20 native SOL transfers in the batch")
308+
309+
// Verify each transfer
310+
for i, tx := range nativeTransfers {
311+
assert.Equal(t, txSig, tx.TxHash, "transfer %d: TxHash should match", i)
312+
assert.Equal(t, expectedSender, tx.FromAddress, "transfer %d: sender should match", i)
313+
assert.Equal(t, expectedRecipients[i], tx.ToAddress, "transfer %d: recipient should match", i)
314+
assert.Equal(t, "1", tx.Amount, "transfer %d: amount should be 1 lamport", i)
315+
assert.Empty(t, tx.AssetAddress, "transfer %d: AssetAddress should be empty for native SOL", i)
316+
}
317+
318+
// Verify no token transfers were detected
319+
for _, tx := range transfers {
320+
assert.NotEqual(t, constant.TxTypeTokenTransfer, tx.Type, "should not detect any token transfers")
321+
}
322+
323+
t.Logf("Batch transfer: %d native SOL transfers from %s (1 lamport each)", len(nativeTransfers), expectedSender)
324+
}
325+
326+
// TestParseSquadsMultisigTransfer tests parsing of a Squads Multisig (SMPLecH534NA9acpos4G6x7uf3LWbCAwZQE9e8ZekMu)
327+
// transaction that CPI-calls Token-2022 with transferCheckedWithFee.
328+
// Transaction: 2Sqm2FwnSdTLQ5tfbpCKafWiQDwynmDZrkvGxCbpin3mwPHdmRKNDq6drv5rnosp9is86JwScQLnyWHbV9TfYTDj
329+
func TestParseSquadsMultisigTransfer(t *testing.T) {
330+
if testing.Short() {
331+
t.Skip("skipping integration test")
332+
}
333+
334+
ctx := context.Background()
335+
c := newTestSolanaClient()
336+
337+
txSig := "2Sqm2FwnSdTLQ5tfbpCKafWiQDwynmDZrkvGxCbpin3mwPHdmRKNDq6drv5rnosp9is86JwScQLnyWHbV9TfYTDj"
338+
txResult, err := c.GetTransaction(ctx, txSig)
339+
require.NoError(t, err)
340+
require.NotNil(t, txResult, "transaction should exist on mainnet")
341+
342+
idx := newTestSolanaIndexer()
343+
block := txToBlockResult(txResult)
344+
345+
ts := uint64(0)
346+
if txResult.BlockTime != nil {
347+
ts = uint64(*txResult.BlockTime)
348+
}
349+
transfers := idx.extractSolanaTransfers("solana-mainnet", txResult.Slot, ts, block)
350+
351+
// This transaction contains a Token-2022 transferCheckedWithFee via Squads multisig CPI.
352+
// Token: HeLp6NuQkmYB4pYWo2zYs22mESHXPQYzXbB8n4V98jwC, Amount: 13000000 (0.013 with 9 decimals)
353+
var tokenTransfer *types.Transaction
354+
for i := range transfers {
355+
if transfers[i].Type == constant.TxTypeTokenTransfer {
356+
tokenTransfer = &transfers[i]
357+
break
358+
}
359+
}
360+
361+
require.NotNil(t, tokenTransfer, "should detect the Token-2022 transferCheckedWithFee from inner instructions")
362+
363+
assert.Equal(t, txSig, tokenTransfer.TxHash, "TxHash should match")
364+
assert.Equal(t, "13000000", tokenTransfer.Amount, "Amount should be 13000000")
365+
assert.Equal(t, "HeLp6NuQkmYB4pYWo2zYs22mESHXPQYzXbB8n4V98jwC", tokenTransfer.AssetAddress, "AssetAddress should be the token mint")
366+
assert.Equal(t, "BfV1aVZTcN2R8fA9Asa49qsfYzX3rH3jVGkwWAA2muDs", tokenTransfer.FromAddress, "FromAddress should be the source owner")
367+
assert.Equal(t, "CvySeQ1FR4CgKo4N1L6UhgCaJ8RWJbcXi32ZppCAnRF4", tokenTransfer.ToAddress, "ToAddress should be the destination owner")
368+
369+
t.Logf("Squads multisig transfer: from=%s to=%s amount=%s token=%s",
370+
tokenTransfer.FromAddress, tokenTransfer.ToAddress,
371+
tokenTransfer.Amount, tokenTransfer.AssetAddress)
372+
}

0 commit comments

Comments
 (0)