/
githubmirror
/
client
Обзор
Документация
Войти
/
githubmirror
/
client
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
go/stellar/batch.go
403 строки
13 KB
chrisnojima-zoom
Version 670 - clean2 (#29122)
08 июн 2026, 19:31
Не верифицирован
08 июн 2026, 19:31
1943197
Код
Авторство
О чём код?
package stellar import ( "errors" "fmt" "sort" "sync" "time" "github.com/keybase/client/go/libkb" "github.com/keybase/client/go/protocol/keybase1" "github.com/keybase/client/go/protocol/stellar1" "github.com/keybase/client/go/stellar/relays" "github.com/keybase/client/go/stellar/remote" "github.com/keybase/client/go/stellar/stellarcommon" "github.com/keybase/stellarnet" "github.com/stellar/go/build" ) const ( minAmountRelayXLM = "2.01" minAmountCreateAccountXLM = "1" ) // Batch sends a batch of payments from the user to multiple recipients in // a time-efficient manner. func Batch(mctx libkb.MetaContext, walletState *WalletState, arg stellar1.BatchLocalArg) (res stellar1.BatchResultLocal, err error) { mctx = mctx.WithLogTag("BATCH=" + arg.BatchID) startTime := time.Now() res.StartTime = stellar1.ToTimeMs(startTime) defer func() { if res.EndTime == 0 { res.EndTime = stellar1.ToTimeMs(time.Now()) } }() // look up sender account senderAccountID, senderSeed, err := LookupSenderSeed(mctx) if err != nil { return res, err } mctx.Debug("Batch sender account ID: %s", senderAccountID) mctx.Debug("Batch size: %d", len(arg.Payments)) // prepare the payments prepared, unlock, err := PrepareBatchPayments(mctx, walletState, senderSeed, arg.Payments, arg.BatchID) if err != nil { walletState.SeqnoUnlock() return res, err } res.PreparedTime = stellar1.ToTimeMs(time.Now()) // make a listener that will get payment status updates listenerID, listenerCh, err := DefaultLoader(mctx.G()).GetListener() if err != nil { unlock() return res, err } defer DefaultLoader(mctx.G()).RemoveListener(listenerID) resultList := make([]stellar1.BatchPaymentResult, len(prepared)) waiting := make(map[stellar1.TransactionID]int) // submit the payments // need to submit tx one at a time, in order for i := range prepared { if prepared[i] == nil { unlock() // this should never happen return res, errors.New("batch prepare failed") } bpResult := stellar1.BatchPaymentResult{ Username: prepared[i].Username.String(), StartTime: stellar1.ToTimeMs(time.Now()), } if prepared[i].Error != nil { makeResultError(&bpResult, prepared[i].Error) } else { submitBatchTx(mctx, walletState, senderAccountID, prepared[i], &bpResult) if bpResult.Status == stellar1.PaymentStatus_PENDING { // add the tx id and the index of this payment to a waiting list waiting[bpResult.TxID] = i } } bpResult.StatusDescription = stellar1.PaymentStatusRevMap[bpResult.Status] resultList[i] = bpResult } // release the lock before waiting for the payments unlock() res.AllSubmittedTime = stellar1.ToTimeMs(time.Now()) // wait for the payments waitingCount := len(waiting) mctx.Debug("waiting for %d payments to complete", waitingCount) timedOut := false var chatWaitGroup sync.WaitGroup for waitingCount > 0 && !timedOut { select { case <-time.After(5 * time.Second): if time.Since(startTime) > time.Duration(arg.TimeoutSecs)*time.Second { mctx.Debug("ran out of time waiting for tx status updates (%d remaining)", waitingCount) timedOut = true } case update := <-listenerCh: index, ok := waiting[update.TxID] if ok { mctx.Debug("received status update for %s: %s", update.TxID, update.Status) resultList[index].Status = update.Status resultList[index].StatusDescription = stellar1.PaymentStatusRevMap[update.Status] if update.Status != stellar1.PaymentStatus_PENDING { waitingCount-- resultList[index].EndTime = stellar1.ToTimeMs(time.Now()) delete(waiting, update.TxID) mctx.Debug("no longer waiting for %s status updates (%d remaining)", update.TxID, waitingCount) } if update.Status == stellar1.PaymentStatus_COMPLETED || update.Status == stellar1.PaymentStatus_CLAIMABLE { chatWaitGroup.Add(1) go func(m libkb.MetaContext, recipient string, txID stellar1.TransactionID) { if err := chatSendPaymentMessage(m, recipient, txID, true); err != nil { m.Debug("chatSendPaymentMessageTo %s (%s): error: %s", recipient, txID, err) } else { m.Debug("chatSendPaymentMessageTo %s (%s): success", recipient, txID) } chatWaitGroup.Done() }(mctx.BackgroundWithLogTags(), resultList[index].Username, update.TxID) } } } } res.AllCompleteTime = stellar1.ToTimeMs(time.Now()) mctx.Debug("done waiting for payments to complete") mctx.Debug("waiting for chat messages to finish sending") chatWaitGroup.Wait() mctx.Debug("done waiting for chat messages to finish sending") res.Payments = resultList res.EndTime = stellar1.ToTimeMs(time.Now()) calculateStats(&res) return res, nil } // PrepareBatchPayments prepares a list of payments to be submitted. // Each payment is prepared concurrently. // (this is an exposed function to make testing from outside this package easier) func PrepareBatchPayments(mctx libkb.MetaContext, walletState *WalletState, senderSeed stellarnet.SeedStr, payments []stellar1.BatchPaymentArg, batchID string) ([]*MiniPrepared, func(), error) { mctx.Debug("preparing %d batch payments", len(payments)) baseFee := walletState.BaseFee(mctx) prepared := make(chan *MiniPrepared) sp, unlock := NewSeqnoProvider(mctx, walletState) for _, payment := range payments { go func(p stellar1.BatchPaymentArg) { prepared <- prepareBatchPayment(mctx, walletState, sp, senderSeed, p, baseFee, batchID) }(payment) } // prepared chan could be out of order, so sort by seqno preparedList := make([]*MiniPrepared, len(payments)) for i := range payments { preparedList[i] = <-prepared } sort.Slice(preparedList, func(a, b int) bool { return preparedList[a].Seqno < preparedList[b].Seqno }) return preparedList, unlock, nil } func prepareBatchPayment(mctx libkb.MetaContext, remoter remote.Remoter, sp build.SequenceProvider, senderSeed stellarnet.SeedStr, payment stellar1.BatchPaymentArg, baseFee uint64, batchID string) *MiniPrepared { recipient, err := LookupRecipient(mctx, stellarcommon.RecipientInput(payment.Recipient), false /* isCLI for identify purposes */) if err != nil { mctx.Debug("LookupRecipient error: %s", err) return &MiniPrepared{ Username: libkb.NewNormalizedUsername(payment.Recipient), Error: errors.New("error looking up recipient"), } } if recipient.AccountID == nil { return prepareBatchPaymentRelay(mctx, remoter, sp, senderSeed, payment, recipient, baseFee, batchID) } return prepareBatchPaymentDirect(mctx, remoter, sp, senderSeed, payment, recipient, baseFee, batchID) } func prepareBatchPaymentDirect(mctx libkb.MetaContext, remoter remote.Remoter, sp build.SequenceProvider, senderSeed stellarnet.SeedStr, payment stellar1.BatchPaymentArg, recipient stellarcommon.Recipient, baseFee uint64, batchID string) *MiniPrepared { result := &MiniPrepared{Username: libkb.NewNormalizedUsername(payment.Recipient)} funded, err := isAccountFunded(mctx.Ctx(), remoter, stellar1.AccountID(recipient.AccountID.String())) if err != nil { result.Error = err return result } if !funded { if isAmountLessThanMin(payment.Amount, minAmountCreateAccountXLM) { result.Error = fmt.Errorf("you must send at least %s XLM to fund the account for %s", minAmountCreateAccountXLM, payment.Recipient) return result } } result.Direct = &stellar1.PaymentDirectPost{ FromDeviceID: mctx.G().ActiveDevice.DeviceID(), To: &recipient.User.UV, QuickReturn: true, BatchID: batchID, } var signResult stellarnet.SignResult if funded { signResult, err = stellarnet.PaymentXLMTransactionWithMemo(senderSeed, *recipient.AccountID, payment.Amount, stellarnet.NewMemoNone(), sp, nil, baseFee) } else { signResult, err = stellarnet.CreateAccountXLMTransaction(senderSeed, *recipient.AccountID, payment.Amount, "", sp, nil, baseFee) } if err != nil { result.Error = err return result } if len(payment.Message) > 0 { noteClear := stellar1.NoteContents{ Note: payment.Message, StellarID: stellar1.TransactionID(signResult.TxHash), } var recipientUv *keybase1.UserVersion if recipient.User != nil { recipientUv = &recipient.User.UV } result.Direct.NoteB64, err = NoteEncryptB64(mctx, noteClear, recipientUv) if err != nil { result.Error = fmt.Errorf("error encrypting note: %v", err) return result } } result.Direct.SignedTransaction = signResult.Signed result.Seqno = signResult.Seqno result.TxID = stellar1.TransactionID(signResult.TxHash) return result } func prepareBatchPaymentRelay(mctx libkb.MetaContext, remoter remote.Remoter, sp build.SequenceProvider, senderSeed stellarnet.SeedStr, payment stellar1.BatchPaymentArg, recipient stellarcommon.Recipient, baseFee uint64, batchID string) *MiniPrepared { result := &MiniPrepared{Username: libkb.NewNormalizedUsername(payment.Recipient)} if isAmountLessThanMin(payment.Amount, minAmountRelayXLM) { result.Error = fmt.Errorf("you must send at least %s XLM to fund the account for %s", minAmountRelayXLM, payment.Recipient) return result } appKey, teamID, err := relays.GetKey(mctx, recipient) if err != nil { result.Error = err return result } relay, err := relays.Create(relays.Input{ From: stellar1.SecretKey(senderSeed), AmountXLM: payment.Amount, Note: payment.Message, EncryptFor: appKey, SeqnoProvider: sp, Timebounds: nil, BaseFee: baseFee, }) if err != nil { result.Error = err return result } post := stellar1.PaymentRelayPost{ FromDeviceID: mctx.ActiveDevice().DeviceID(), ToAssertion: string(recipient.Input), RelayAccount: relay.RelayAccountID, TeamID: teamID, BoxB64: relay.EncryptedB64, SignedTransaction: relay.FundTx.Signed, QuickReturn: true, BatchID: batchID, } if recipient.User != nil { post.To = &recipient.User.UV } result.Relay = &post result.Seqno = relay.FundTx.Seqno result.TxID = stellar1.TransactionID(relay.FundTx.TxHash) return result } func calculateStats(res *stellar1.BatchResultLocal) { res.OverallDurationMs = res.EndTime - res.StartTime res.PrepareDurationMs = res.PreparedTime - res.StartTime res.SubmitDurationMs = res.AllSubmittedTime - res.PreparedTime res.WaitPaymentsDurationMs = res.AllCompleteTime - res.AllSubmittedTime res.WaitChatDurationMs = res.EndTime - res.AllCompleteTime var durationTotal stellar1.TimeMs var durationSuccess stellar1.TimeMs var durationDirect stellar1.TimeMs var durationRelay stellar1.TimeMs var durationError stellar1.TimeMs var countDone int64 for _, p := range res.Payments { duration := p.EndTime - p.StartTime durationTotal += duration switch p.Status { case stellar1.PaymentStatus_COMPLETED: countDone++ res.CountSuccess++ res.CountDirect++ durationSuccess += duration durationDirect += duration case stellar1.PaymentStatus_CLAIMABLE: countDone++ res.CountSuccess++ res.CountRelay++ durationSuccess += duration durationRelay += duration case stellar1.PaymentStatus_PENDING: res.CountPending++ default: // error countDone++ res.CountError++ durationError += duration } } if countDone > 0 { res.AvgDurationMs = stellar1.TimeMs(int64(durationTotal) / countDone) } if res.CountSuccess > 0 { res.AvgSuccessDurationMs = stellar1.TimeMs(int64(durationSuccess) / int64(res.CountSuccess)) } if res.CountDirect > 0 { res.AvgDirectDurationMs = stellar1.TimeMs(int64(durationDirect) / int64(res.CountDirect)) } if res.CountRelay > 0 { res.AvgRelayDurationMs = stellar1.TimeMs(int64(durationRelay) / int64(res.CountRelay)) } if res.CountError > 0 { res.AvgErrorDurationMs = stellar1.TimeMs(int64(durationError) / int64(res.CountError)) } } func makeResultError(res *stellar1.BatchPaymentResult, err error) { res.EndTime = stellar1.ToTimeMs(time.Now()) res.Error = &stellar1.BatchPaymentError{Message: err.Error()} res.Status = stellar1.PaymentStatus_ERROR } func submitBatchTx(mctx libkb.MetaContext, walletState *WalletState, senderAccountID stellar1.AccountID, prepared *MiniPrepared, bpResult *stellar1.BatchPaymentResult) { mctx.Debug("submitting batch payment seqno %d", prepared.Seqno) err := walletState.AddPendingTx(mctx.Ctx(), senderAccountID, prepared.TxID, prepared.Seqno) if err != nil { // it's ok to keep going here mctx.Debug("error calling AddPendingTx: %s", err) } var submitRes stellar1.PaymentResult switch { case prepared.Direct != nil: submitRes, err = walletState.SubmitPayment(mctx.Ctx(), *prepared.Direct) case prepared.Relay != nil: submitRes, err = walletState.SubmitRelayPayment(mctx.Ctx(), *prepared.Relay) default: err = errors.New("no prepared direct or relay payment") } bpResult.SubmittedTime = stellar1.ToTimeMs(time.Now()) if err != nil { mctx.Debug("error submitting batch payment seqno %d, txid %s: %s", prepared.Seqno, prepared.TxID, err) makeResultError(bpResult, err) return } bpResult.TxID = submitRes.StellarID if submitRes.Pending { bpResult.Status = stellar1.PaymentStatus_PENDING } else { bpResult.Status = stellar1.PaymentStatus_COMPLETED bpResult.EndTime = stellar1.ToTimeMs(time.Now()) } }