@@ -229,6 +229,26 @@ type (
229229 Usage rhp4.Usage `json:"usage"`
230230 }
231231
232+ // A PoolBalance pairs a pool key with its current balance.
233+ PoolBalance struct {
234+ Pool rhp4.Account `json:"pool"`
235+ Balance types.Currency `json:"balance"`
236+ }
237+
238+ // RPCReplenishPoolsParams contains the parameters for the replenish pools RPC.
239+ RPCReplenishPoolsParams struct {
240+ Pools []rhp4.Account `json:"pools"`
241+ Target types.Currency `json:"target"`
242+ Contract ContractRevision `json:"contract"`
243+ }
244+
245+ // RPCReplenishPoolsResult contains the result of executing the replenish pools RPC.
246+ RPCReplenishPoolsResult struct {
247+ Revision types.V2FileContract `json:"revision"`
248+ Deposits []rhp4.AccountDeposit `json:"deposits"`
249+ Usage rhp4.Usage `json:"usage"`
250+ }
251+
232252 // RPCSectorRootsResult contains the result of executing the sector roots RPC.
233253 RPCSectorRootsResult struct {
234254 Revision types.V2FileContract `json:"revision"`
@@ -815,6 +835,149 @@ func RPCReplenishAccounts(ctx context.Context, t TransportClient, p RPCReplenish
815835 }, nil
816836}
817837
838+ // RPCReplenishPools tops up pool balances to a target on the host. Reuses
839+ // RPCReplenishAccounts wire types — the host routes deposits to its pool
840+ // table based on the RPC ID.
841+ func RPCReplenishPools (ctx context.Context , t TransportClient , p RPCReplenishPoolsParams , cs consensus.State , signer ContractSigner ) (RPCReplenishPoolsResult , error ) {
842+ req := rhp4.RPCReplenishAccountsRequest {
843+ Accounts : p .Pools ,
844+ Target : p .Target ,
845+ ContractID : p .Contract .ID ,
846+ }
847+ challengeSigHash := req .ChallengeSigHash (p .Contract .Revision .RevisionNumber )
848+ req .ChallengeSignature = signer .SignHash (challengeSigHash )
849+
850+ if err := req .Validate (); err != nil {
851+ return RPCReplenishPoolsResult {}, fmt .Errorf ("invalid request: %w" , err )
852+ }
853+
854+ s , err := openStream (ctx , t , defaultStreamTimeout )
855+ if err != nil {
856+ return RPCReplenishPoolsResult {}, fmt .Errorf ("failed to dial stream: %w" , err )
857+ }
858+ defer s .Close ()
859+
860+ if err := rhp4 .WriteRequest (s , rhp4 .RPCReplenishPoolsID , & req ); err != nil {
861+ return RPCReplenishPoolsResult {}, fmt .Errorf ("failed to write request: %w" , err )
862+ }
863+
864+ maxCost := p .Target .Mul64 (uint64 (len (p .Pools )))
865+
866+ var resp rhp4.RPCReplenishAccountsResponse
867+ if err := rhp4 .ReadResponse (s , & resp ); err != nil {
868+ return RPCReplenishPoolsResult {}, fmt .Errorf ("failed to read response: %w" , err )
869+ } else if len (resp .Deposits ) != len (p .Pools ) {
870+ return RPCReplenishPoolsResult {}, fmt .Errorf ("expected %v deposits, got %v" , len (p .Pools ), len (resp .Deposits ))
871+ }
872+
873+ for _ , deposit := range resp .Deposits {
874+ if deposit .Amount .Cmp (p .Target ) > 0 {
875+ return RPCReplenishPoolsResult {}, fmt .Errorf ("expected deposit <= %v, got %v" , p .Target , deposit .Amount )
876+ }
877+ }
878+
879+ totalCost := resp .TotalCost ()
880+ if totalCost .IsZero () {
881+ return RPCReplenishPoolsResult {
882+ Revision : p .Contract .Revision ,
883+ Deposits : resp .Deposits ,
884+ }, nil
885+ } else if totalCost .Cmp (maxCost ) > 0 {
886+ return RPCReplenishPoolsResult {}, fmt .Errorf ("expected cost <= %v, got %v" , maxCost , totalCost )
887+ }
888+
889+ revision , usage , err := rhp4 .ReviseForReplenish (p .Contract .Revision , totalCost )
890+ if err != nil {
891+ return RPCReplenishPoolsResult {}, fmt .Errorf ("failed to revise contract: %w" , err )
892+ }
893+
894+ sigHash := cs .ContractSigHash (revision )
895+ revision .RenterSignature = signer .SignHash (sigHash )
896+
897+ signatureResp := rhp4.RPCReplenishAccountsSecondResponse {
898+ RenterSignature : revision .RenterSignature ,
899+ }
900+ if err := rhp4 .WriteResponse (s , & signatureResp ); err != nil {
901+ return RPCReplenishPoolsResult {}, fmt .Errorf ("failed to write signature response: %w" , err )
902+ }
903+
904+ var hostSignature rhp4.RPCReplenishAccountsThirdResponse
905+ if err := rhp4 .ReadResponse (s , & hostSignature ); err != nil {
906+ return RPCReplenishPoolsResult {}, fmt .Errorf ("failed to read host signatures: %w" , err )
907+ } else if ! p .Contract .Revision .HostPublicKey .VerifyHash (sigHash , hostSignature .HostSignature ) {
908+ return RPCReplenishPoolsResult {}, fmt .Errorf ("failed to validate host signature: %w" , rhp4 .ErrInvalidSignature )
909+ }
910+ revision .HostSignature = hostSignature .HostSignature
911+ return RPCReplenishPoolsResult {
912+ Revision : revision ,
913+ Deposits : resp .Deposits ,
914+ Usage : usage ,
915+ }, nil
916+ }
917+
918+ // PoolAttachInput pairs an account with the pool keypair authorizing the
919+ // attachment.
920+ type PoolAttachInput struct {
921+ Account rhp4.Account
922+ PoolKey types.PrivateKey
923+ }
924+
925+ // PoolDetachInput identifies an attachment to sever, paired with the private
926+ // key that signs the request. The signer may be either the account's or the
927+ // pool's key.
928+ type PoolDetachInput struct {
929+ Account rhp4.Account
930+ Pool rhp4.Account
931+ Signer types.PrivateKey
932+ }
933+
934+ // RPCAttachPools batches one or more attachments. Each entry is signed by
935+ // its pool's private key. validity bounds the replay window for the whole
936+ // batch.
937+ func RPCAttachPools (ctx context.Context , t TransportClient , inputs []PoolAttachInput , validity time.Duration ) error {
938+ hostKey := t .PeerKey ()
939+ deadline := time .Now ().Add (validity )
940+ attachments := make ([]rhp4.PoolAttachment , 0 , len (inputs ))
941+ for _ , in := range inputs {
942+ a := rhp4.PoolAttachment {
943+ Account : in .Account ,
944+ Pool : rhp4 .Account (in .PoolKey .PublicKey ()),
945+ ValidUntil : deadline ,
946+ }
947+ a .Signature = in .PoolKey .SignHash (a .SigHash (hostKey ))
948+ attachments = append (attachments , a )
949+ }
950+ req := rhp4.RPCAttachPoolsRequest {Attachments : attachments }
951+ if err := req .Validate (); err != nil {
952+ return fmt .Errorf ("invalid request: %w" , err )
953+ }
954+ var resp rhp4.RPCAttachPoolsResponse
955+ return callSingleRoundtripRPC (ctx , t , rhp4 .RPCAttachPoolsID , & req , & resp )
956+ }
957+
958+ // RPCDetachPools batches one or more detachments. Each entry is signed by
959+ // either the account's or the pool's private key.
960+ func RPCDetachPools (ctx context.Context , t TransportClient , inputs []PoolDetachInput , validity time.Duration ) error {
961+ hostKey := t .PeerKey ()
962+ deadline := time .Now ().Add (validity )
963+ detachments := make ([]rhp4.PoolDetachment , 0 , len (inputs ))
964+ for _ , in := range inputs {
965+ d := rhp4.PoolDetachment {
966+ Account : in .Account ,
967+ Pool : in .Pool ,
968+ ValidUntil : deadline ,
969+ }
970+ d .Signature = in .Signer .SignHash (d .SigHash (hostKey ))
971+ detachments = append (detachments , d )
972+ }
973+ req := rhp4.RPCDetachPoolsRequest {Detachments : detachments }
974+ if err := req .Validate (); err != nil {
975+ return fmt .Errorf ("invalid request: %w" , err )
976+ }
977+ var resp rhp4.RPCDetachPoolsResponse
978+ return callSingleRoundtripRPC (ctx , t , rhp4 .RPCDetachPoolsID , & req , & resp )
979+ }
980+
818981// RPCLatestRevision returns the latest revision of a contract.
819982func RPCLatestRevision (ctx context.Context , t TransportClient , contractID types.FileContractID ) (resp rhp4.RPCLatestRevisionResponse , err error ) {
820983 req := rhp4.RPCLatestRevisionRequest {ContractID : contractID }
0 commit comments