diff --git a/api-spec/openapi/swagger/ark/v1/service.swagger.json b/api-spec/openapi/swagger/ark/v1/service.swagger.json index 9966c68..5937a58 100644 --- a/api-spec/openapi/swagger/ark/v1/service.swagger.json +++ b/api-spec/openapi/swagger/ark/v1/service.swagger.json @@ -1070,7 +1070,7 @@ "swept": { "type": "boolean" }, - "pending": { + "isOor": { "type": "boolean" }, "redeemTx": { diff --git a/api-spec/protobuf/ark/v1/service.proto b/api-spec/protobuf/ark/v1/service.proto index b924eb6..cb44c0d 100755 --- a/api-spec/protobuf/ark/v1/service.proto +++ b/api-spec/protobuf/ark/v1/service.proto @@ -318,7 +318,7 @@ message Vtxo { string spent_by = 4; int64 expire_at = 5; bool swept = 6; - bool pending = 7; + bool is_oor = 7; string redeem_tx = 8; uint64 amount = 9; string pubkey = 10; diff --git a/api-spec/protobuf/gen/ark/v1/service.pb.go b/api-spec/protobuf/gen/ark/v1/service.pb.go index 10e6dcc..2625f4a 100644 --- a/api-spec/protobuf/gen/ark/v1/service.pb.go +++ b/api-spec/protobuf/gen/ark/v1/service.pb.go @@ -2436,7 +2436,7 @@ type Vtxo struct { SpentBy string `protobuf:"bytes,4,opt,name=spent_by,json=spentBy,proto3" json:"spent_by,omitempty"` ExpireAt int64 `protobuf:"varint,5,opt,name=expire_at,json=expireAt,proto3" json:"expire_at,omitempty"` Swept bool `protobuf:"varint,6,opt,name=swept,proto3" json:"swept,omitempty"` - Pending bool `protobuf:"varint,7,opt,name=pending,proto3" json:"pending,omitempty"` + IsOor bool `protobuf:"varint,7,opt,name=is_oor,json=isOor,proto3" json:"is_oor,omitempty"` RedeemTx string `protobuf:"bytes,8,opt,name=redeem_tx,json=redeemTx,proto3" json:"redeem_tx,omitempty"` Amount uint64 `protobuf:"varint,9,opt,name=amount,proto3" json:"amount,omitempty"` Pubkey string `protobuf:"bytes,10,opt,name=pubkey,proto3" json:"pubkey,omitempty"` @@ -2516,9 +2516,9 @@ func (x *Vtxo) GetSwept() bool { return false } -func (x *Vtxo) GetPending() bool { +func (x *Vtxo) GetIsOor() bool { if x != nil { - return x.Pending + return x.IsOor } return false } @@ -3067,7 +3067,7 @@ var file_ark_v1_service_proto_rawDesc = []byte{ 0x52, 0x04, 0x74, 0x78, 0x69, 0x64, 0x12, 0x0e, 0x0a, 0x02, 0x74, 0x78, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x74, 0x78, 0x12, 0x1f, 0x0a, 0x0b, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x78, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x70, 0x61, 0x72, - 0x65, 0x6e, 0x74, 0x54, 0x78, 0x69, 0x64, 0x22, 0x9e, 0x02, 0x0a, 0x04, 0x56, 0x74, 0x78, 0x6f, + 0x65, 0x6e, 0x74, 0x54, 0x78, 0x69, 0x64, 0x22, 0x9b, 0x02, 0x0a, 0x04, 0x56, 0x74, 0x78, 0x6f, 0x12, 0x2c, 0x0a, 0x08, 0x6f, 0x75, 0x74, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x75, 0x74, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x52, 0x08, 0x6f, 0x75, 0x74, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x14, @@ -3079,177 +3079,177 @@ var file_ark_v1_service_proto_rawDesc = []byte{ 0x0a, 0x09, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x5f, 0x61, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x41, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x77, 0x65, 0x70, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x73, 0x77, 0x65, 0x70, - 0x74, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x18, 0x07, 0x20, 0x01, - 0x28, 0x08, 0x52, 0x07, 0x70, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x12, 0x1b, 0x0a, 0x09, 0x72, - 0x65, 0x64, 0x65, 0x65, 0x6d, 0x5f, 0x74, 0x78, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, - 0x72, 0x65, 0x64, 0x65, 0x65, 0x6d, 0x54, 0x78, 0x12, 0x16, 0x0a, 0x06, 0x61, 0x6d, 0x6f, 0x75, - 0x6e, 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, - 0x12, 0x16, 0x0a, 0x06, 0x70, 0x75, 0x62, 0x6b, 0x65, 0x79, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x06, 0x70, 0x75, 0x62, 0x6b, 0x65, 0x79, 0x22, 0x1e, 0x0a, 0x1c, 0x47, 0x65, 0x74, 0x54, - 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x53, 0x74, 0x72, 0x65, 0x61, - 0x6d, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x8c, 0x01, 0x0a, 0x1d, 0x47, 0x65, 0x74, - 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x53, 0x74, 0x72, 0x65, - 0x61, 0x6d, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x30, 0x0a, 0x05, 0x72, 0x6f, - 0x75, 0x6e, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x61, 0x72, 0x6b, 0x2e, - 0x76, 0x31, 0x2e, 0x52, 0x6f, 0x75, 0x6e, 0x64, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, - 0x69, 0x6f, 0x6e, 0x48, 0x00, 0x52, 0x05, 0x72, 0x6f, 0x75, 0x6e, 0x64, 0x12, 0x33, 0x0a, 0x06, - 0x72, 0x65, 0x64, 0x65, 0x65, 0x6d, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x61, - 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x64, 0x65, 0x65, 0x6d, 0x54, 0x72, 0x61, 0x6e, - 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x00, 0x52, 0x06, 0x72, 0x65, 0x64, 0x65, 0x65, - 0x6d, 0x42, 0x04, 0x0a, 0x02, 0x74, 0x78, 0x22, 0xd8, 0x01, 0x0a, 0x10, 0x52, 0x6f, 0x75, 0x6e, - 0x64, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, - 0x74, 0x78, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x78, 0x69, 0x64, - 0x12, 0x31, 0x0a, 0x0b, 0x73, 0x70, 0x65, 0x6e, 0x74, 0x5f, 0x76, 0x74, 0x78, 0x6f, 0x73, 0x18, - 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x4f, - 0x75, 0x74, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x52, 0x0a, 0x73, 0x70, 0x65, 0x6e, 0x74, 0x56, 0x74, - 0x78, 0x6f, 0x73, 0x12, 0x35, 0x0a, 0x0f, 0x73, 0x70, 0x65, 0x6e, 0x64, 0x61, 0x62, 0x6c, 0x65, - 0x5f, 0x76, 0x74, 0x78, 0x6f, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x61, - 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x56, 0x74, 0x78, 0x6f, 0x52, 0x0e, 0x73, 0x70, 0x65, 0x6e, - 0x64, 0x61, 0x62, 0x6c, 0x65, 0x56, 0x74, 0x78, 0x6f, 0x73, 0x12, 0x46, 0x0a, 0x16, 0x63, 0x6c, - 0x61, 0x69, 0x6d, 0x65, 0x64, 0x5f, 0x62, 0x6f, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x5f, 0x75, - 0x74, 0x78, 0x6f, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x72, 0x6b, - 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x75, 0x74, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x52, 0x14, 0x63, 0x6c, - 0x61, 0x69, 0x6d, 0x65, 0x64, 0x42, 0x6f, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x55, 0x74, 0x78, - 0x6f, 0x73, 0x22, 0x91, 0x01, 0x0a, 0x11, 0x52, 0x65, 0x64, 0x65, 0x65, 0x6d, 0x54, 0x72, 0x61, - 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x78, 0x69, 0x64, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x78, 0x69, 0x64, 0x12, 0x31, 0x0a, 0x0b, - 0x73, 0x70, 0x65, 0x6e, 0x74, 0x5f, 0x76, 0x74, 0x78, 0x6f, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, - 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x75, 0x74, 0x70, 0x6f, - 0x69, 0x6e, 0x74, 0x52, 0x0a, 0x73, 0x70, 0x65, 0x6e, 0x74, 0x56, 0x74, 0x78, 0x6f, 0x73, 0x12, - 0x35, 0x0a, 0x0f, 0x73, 0x70, 0x65, 0x6e, 0x64, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x76, 0x74, 0x78, - 0x6f, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, - 0x31, 0x2e, 0x56, 0x74, 0x78, 0x6f, 0x52, 0x0e, 0x73, 0x70, 0x65, 0x6e, 0x64, 0x61, 0x62, 0x6c, - 0x65, 0x56, 0x74, 0x78, 0x6f, 0x73, 0x2a, 0x98, 0x01, 0x0a, 0x0a, 0x52, 0x6f, 0x75, 0x6e, 0x64, - 0x53, 0x74, 0x61, 0x67, 0x65, 0x12, 0x1b, 0x0a, 0x17, 0x52, 0x4f, 0x55, 0x4e, 0x44, 0x5f, 0x53, - 0x54, 0x41, 0x47, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, - 0x10, 0x00, 0x12, 0x1c, 0x0a, 0x18, 0x52, 0x4f, 0x55, 0x4e, 0x44, 0x5f, 0x53, 0x54, 0x41, 0x47, - 0x45, 0x5f, 0x52, 0x45, 0x47, 0x49, 0x53, 0x54, 0x52, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x10, 0x01, - 0x12, 0x1c, 0x0a, 0x18, 0x52, 0x4f, 0x55, 0x4e, 0x44, 0x5f, 0x53, 0x54, 0x41, 0x47, 0x45, 0x5f, - 0x46, 0x49, 0x4e, 0x41, 0x4c, 0x49, 0x5a, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x10, 0x02, 0x12, 0x19, - 0x0a, 0x15, 0x52, 0x4f, 0x55, 0x4e, 0x44, 0x5f, 0x53, 0x54, 0x41, 0x47, 0x45, 0x5f, 0x46, 0x49, - 0x4e, 0x41, 0x4c, 0x49, 0x5a, 0x45, 0x44, 0x10, 0x03, 0x12, 0x16, 0x0a, 0x12, 0x52, 0x4f, 0x55, - 0x4e, 0x44, 0x5f, 0x53, 0x54, 0x41, 0x47, 0x45, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x45, 0x44, 0x10, - 0x04, 0x32, 0xeb, 0x0d, 0x0a, 0x0a, 0x41, 0x72, 0x6b, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, - 0x12, 0x4c, 0x0a, 0x07, 0x47, 0x65, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x16, 0x2e, 0x61, 0x72, - 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, - 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x10, 0x82, 0xd3, - 0xe4, 0x93, 0x02, 0x0a, 0x12, 0x08, 0x2f, 0x76, 0x31, 0x2f, 0x69, 0x6e, 0x66, 0x6f, 0x12, 0x74, - 0x0a, 0x12, 0x47, 0x65, 0x74, 0x42, 0x6f, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x41, 0x64, 0x64, - 0x72, 0x65, 0x73, 0x73, 0x12, 0x21, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, + 0x74, 0x12, 0x15, 0x0a, 0x06, 0x69, 0x73, 0x5f, 0x6f, 0x6f, 0x72, 0x18, 0x07, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x05, 0x69, 0x73, 0x4f, 0x6f, 0x72, 0x12, 0x1b, 0x0a, 0x09, 0x72, 0x65, 0x64, 0x65, + 0x65, 0x6d, 0x5f, 0x74, 0x78, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x64, + 0x65, 0x65, 0x6d, 0x54, 0x78, 0x12, 0x16, 0x0a, 0x06, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x18, + 0x09, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x16, 0x0a, + 0x06, 0x70, 0x75, 0x62, 0x6b, 0x65, 0x79, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, + 0x75, 0x62, 0x6b, 0x65, 0x79, 0x22, 0x1e, 0x0a, 0x1c, 0x47, 0x65, 0x74, 0x54, 0x72, 0x61, 0x6e, + 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x8c, 0x01, 0x0a, 0x1d, 0x47, 0x65, 0x74, 0x54, 0x72, 0x61, + 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x30, 0x0a, 0x05, 0x72, 0x6f, 0x75, 0x6e, 0x64, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, + 0x52, 0x6f, 0x75, 0x6e, 0x64, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x48, 0x00, 0x52, 0x05, 0x72, 0x6f, 0x75, 0x6e, 0x64, 0x12, 0x33, 0x0a, 0x06, 0x72, 0x65, 0x64, + 0x65, 0x65, 0x6d, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x61, 0x72, 0x6b, 0x2e, + 0x76, 0x31, 0x2e, 0x52, 0x65, 0x64, 0x65, 0x65, 0x6d, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, + 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x00, 0x52, 0x06, 0x72, 0x65, 0x64, 0x65, 0x65, 0x6d, 0x42, 0x04, + 0x0a, 0x02, 0x74, 0x78, 0x22, 0xd8, 0x01, 0x0a, 0x10, 0x52, 0x6f, 0x75, 0x6e, 0x64, 0x54, 0x72, + 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x78, 0x69, + 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x78, 0x69, 0x64, 0x12, 0x31, 0x0a, + 0x0b, 0x73, 0x70, 0x65, 0x6e, 0x74, 0x5f, 0x76, 0x74, 0x78, 0x6f, 0x73, 0x18, 0x02, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x75, 0x74, 0x70, + 0x6f, 0x69, 0x6e, 0x74, 0x52, 0x0a, 0x73, 0x70, 0x65, 0x6e, 0x74, 0x56, 0x74, 0x78, 0x6f, 0x73, + 0x12, 0x35, 0x0a, 0x0f, 0x73, 0x70, 0x65, 0x6e, 0x64, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x76, 0x74, + 0x78, 0x6f, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x61, 0x72, 0x6b, 0x2e, + 0x76, 0x31, 0x2e, 0x56, 0x74, 0x78, 0x6f, 0x52, 0x0e, 0x73, 0x70, 0x65, 0x6e, 0x64, 0x61, 0x62, + 0x6c, 0x65, 0x56, 0x74, 0x78, 0x6f, 0x73, 0x12, 0x46, 0x0a, 0x16, 0x63, 0x6c, 0x61, 0x69, 0x6d, + 0x65, 0x64, 0x5f, 0x62, 0x6f, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x5f, 0x75, 0x74, 0x78, 0x6f, + 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, + 0x2e, 0x4f, 0x75, 0x74, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x52, 0x14, 0x63, 0x6c, 0x61, 0x69, 0x6d, + 0x65, 0x64, 0x42, 0x6f, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x55, 0x74, 0x78, 0x6f, 0x73, 0x22, + 0x91, 0x01, 0x0a, 0x11, 0x52, 0x65, 0x64, 0x65, 0x65, 0x6d, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, + 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x78, 0x69, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x78, 0x69, 0x64, 0x12, 0x31, 0x0a, 0x0b, 0x73, 0x70, 0x65, + 0x6e, 0x74, 0x5f, 0x76, 0x74, 0x78, 0x6f, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, + 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x75, 0x74, 0x70, 0x6f, 0x69, 0x6e, 0x74, + 0x52, 0x0a, 0x73, 0x70, 0x65, 0x6e, 0x74, 0x56, 0x74, 0x78, 0x6f, 0x73, 0x12, 0x35, 0x0a, 0x0f, + 0x73, 0x70, 0x65, 0x6e, 0x64, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x76, 0x74, 0x78, 0x6f, 0x73, 0x18, + 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x56, + 0x74, 0x78, 0x6f, 0x52, 0x0e, 0x73, 0x70, 0x65, 0x6e, 0x64, 0x61, 0x62, 0x6c, 0x65, 0x56, 0x74, + 0x78, 0x6f, 0x73, 0x2a, 0x98, 0x01, 0x0a, 0x0a, 0x52, 0x6f, 0x75, 0x6e, 0x64, 0x53, 0x74, 0x61, + 0x67, 0x65, 0x12, 0x1b, 0x0a, 0x17, 0x52, 0x4f, 0x55, 0x4e, 0x44, 0x5f, 0x53, 0x54, 0x41, 0x47, + 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, + 0x1c, 0x0a, 0x18, 0x52, 0x4f, 0x55, 0x4e, 0x44, 0x5f, 0x53, 0x54, 0x41, 0x47, 0x45, 0x5f, 0x52, + 0x45, 0x47, 0x49, 0x53, 0x54, 0x52, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x10, 0x01, 0x12, 0x1c, 0x0a, + 0x18, 0x52, 0x4f, 0x55, 0x4e, 0x44, 0x5f, 0x53, 0x54, 0x41, 0x47, 0x45, 0x5f, 0x46, 0x49, 0x4e, + 0x41, 0x4c, 0x49, 0x5a, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x10, 0x02, 0x12, 0x19, 0x0a, 0x15, 0x52, + 0x4f, 0x55, 0x4e, 0x44, 0x5f, 0x53, 0x54, 0x41, 0x47, 0x45, 0x5f, 0x46, 0x49, 0x4e, 0x41, 0x4c, + 0x49, 0x5a, 0x45, 0x44, 0x10, 0x03, 0x12, 0x16, 0x0a, 0x12, 0x52, 0x4f, 0x55, 0x4e, 0x44, 0x5f, + 0x53, 0x54, 0x41, 0x47, 0x45, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x45, 0x44, 0x10, 0x04, 0x32, 0xeb, + 0x0d, 0x0a, 0x0a, 0x41, 0x72, 0x6b, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x4c, 0x0a, + 0x07, 0x47, 0x65, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x16, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, + 0x31, 0x2e, 0x47, 0x65, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x17, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x49, 0x6e, 0x66, + 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x10, 0x82, 0xd3, 0xe4, 0x93, 0x02, + 0x0a, 0x12, 0x08, 0x2f, 0x76, 0x31, 0x2f, 0x69, 0x6e, 0x66, 0x6f, 0x12, 0x74, 0x0a, 0x12, 0x47, + 0x65, 0x74, 0x42, 0x6f, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, + 0x73, 0x12, 0x21, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x42, 0x6f, + 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x42, 0x6f, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, - 0x2e, 0x47, 0x65, 0x74, 0x42, 0x6f, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x41, 0x64, 0x64, 0x72, - 0x65, 0x73, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x17, 0x82, 0xd3, 0xe4, - 0x93, 0x02, 0x11, 0x3a, 0x01, 0x2a, 0x22, 0x0c, 0x2f, 0x76, 0x31, 0x2f, 0x62, 0x6f, 0x61, 0x72, - 0x64, 0x69, 0x6e, 0x67, 0x12, 0x98, 0x01, 0x0a, 0x1a, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, - 0x72, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x73, 0x46, 0x6f, 0x72, 0x4e, 0x65, 0x78, 0x74, 0x52, 0x6f, - 0x75, 0x6e, 0x64, 0x12, 0x29, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x67, - 0x69, 0x73, 0x74, 0x65, 0x72, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x73, 0x46, 0x6f, 0x72, 0x4e, 0x65, - 0x78, 0x74, 0x52, 0x6f, 0x75, 0x6e, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, - 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, - 0x49, 0x6e, 0x70, 0x75, 0x74, 0x73, 0x46, 0x6f, 0x72, 0x4e, 0x65, 0x78, 0x74, 0x52, 0x6f, 0x75, - 0x6e, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x23, 0x82, 0xd3, 0xe4, 0x93, - 0x02, 0x1d, 0x3a, 0x01, 0x2a, 0x22, 0x18, 0x2f, 0x76, 0x31, 0x2f, 0x72, 0x6f, 0x75, 0x6e, 0x64, - 0x2f, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x73, 0x12, - 0x9c, 0x01, 0x0a, 0x1b, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x4f, 0x75, 0x74, 0x70, - 0x75, 0x74, 0x73, 0x46, 0x6f, 0x72, 0x4e, 0x65, 0x78, 0x74, 0x52, 0x6f, 0x75, 0x6e, 0x64, 0x12, - 0x2a, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, - 0x72, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x73, 0x46, 0x6f, 0x72, 0x4e, 0x65, 0x78, 0x74, 0x52, - 0x6f, 0x75, 0x6e, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2b, 0x2e, 0x61, 0x72, - 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x4f, 0x75, 0x74, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x17, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x11, + 0x3a, 0x01, 0x2a, 0x22, 0x0c, 0x2f, 0x76, 0x31, 0x2f, 0x62, 0x6f, 0x61, 0x72, 0x64, 0x69, 0x6e, + 0x67, 0x12, 0x98, 0x01, 0x0a, 0x1a, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x73, 0x46, 0x6f, 0x72, 0x4e, 0x65, 0x78, 0x74, 0x52, 0x6f, 0x75, 0x6e, 0x64, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x24, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1e, - 0x3a, 0x01, 0x2a, 0x22, 0x19, 0x2f, 0x76, 0x31, 0x2f, 0x72, 0x6f, 0x75, 0x6e, 0x64, 0x2f, 0x72, - 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x73, 0x12, 0x7d, - 0x0a, 0x10, 0x53, 0x75, 0x62, 0x6d, 0x69, 0x74, 0x54, 0x72, 0x65, 0x65, 0x4e, 0x6f, 0x6e, 0x63, - 0x65, 0x73, 0x12, 0x1f, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x75, 0x62, 0x6d, - 0x69, 0x74, 0x54, 0x72, 0x65, 0x65, 0x4e, 0x6f, 0x6e, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x75, 0x62, - 0x6d, 0x69, 0x74, 0x54, 0x72, 0x65, 0x65, 0x4e, 0x6f, 0x6e, 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x26, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x20, 0x3a, 0x01, 0x2a, - 0x22, 0x1b, 0x2f, 0x76, 0x31, 0x2f, 0x72, 0x6f, 0x75, 0x6e, 0x64, 0x2f, 0x74, 0x72, 0x65, 0x65, - 0x2f, 0x73, 0x75, 0x62, 0x6d, 0x69, 0x74, 0x4e, 0x6f, 0x6e, 0x63, 0x65, 0x73, 0x12, 0x8d, 0x01, - 0x0a, 0x14, 0x53, 0x75, 0x62, 0x6d, 0x69, 0x74, 0x54, 0x72, 0x65, 0x65, 0x53, 0x69, 0x67, 0x6e, - 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x12, 0x23, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, - 0x53, 0x75, 0x62, 0x6d, 0x69, 0x74, 0x54, 0x72, 0x65, 0x65, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, - 0x75, 0x72, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x61, 0x72, - 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x75, 0x62, 0x6d, 0x69, 0x74, 0x54, 0x72, 0x65, 0x65, 0x53, - 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x22, 0x2a, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x24, 0x3a, 0x01, 0x2a, 0x22, 0x1f, 0x2f, 0x76, - 0x31, 0x2f, 0x72, 0x6f, 0x75, 0x6e, 0x64, 0x2f, 0x74, 0x72, 0x65, 0x65, 0x2f, 0x73, 0x75, 0x62, - 0x6d, 0x69, 0x74, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x12, 0x8e, 0x01, - 0x0a, 0x16, 0x53, 0x75, 0x62, 0x6d, 0x69, 0x74, 0x53, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x46, 0x6f, - 0x72, 0x66, 0x65, 0x69, 0x74, 0x54, 0x78, 0x73, 0x12, 0x25, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, - 0x31, 0x2e, 0x53, 0x75, 0x62, 0x6d, 0x69, 0x74, 0x53, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x46, 0x6f, - 0x72, 0x66, 0x65, 0x69, 0x74, 0x54, 0x78, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x26, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x75, 0x62, 0x6d, 0x69, 0x74, 0x53, - 0x69, 0x67, 0x6e, 0x65, 0x64, 0x46, 0x6f, 0x72, 0x66, 0x65, 0x69, 0x74, 0x54, 0x78, 0x73, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x25, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1f, 0x3a, - 0x01, 0x2a, 0x22, 0x1a, 0x2f, 0x76, 0x31, 0x2f, 0x72, 0x6f, 0x75, 0x6e, 0x64, 0x2f, 0x73, 0x75, - 0x62, 0x6d, 0x69, 0x74, 0x46, 0x6f, 0x72, 0x66, 0x65, 0x69, 0x74, 0x54, 0x78, 0x73, 0x12, 0x6b, - 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, - 0x12, 0x1d, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x45, 0x76, 0x65, - 0x6e, 0x74, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x1e, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x45, 0x76, 0x65, 0x6e, - 0x74, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, - 0x18, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x12, 0x12, 0x10, 0x2f, 0x76, 0x31, 0x2f, 0x72, 0x6f, 0x75, - 0x6e, 0x64, 0x2f, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x30, 0x01, 0x12, 0x56, 0x0a, 0x04, 0x50, - 0x69, 0x6e, 0x67, 0x12, 0x13, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x69, 0x6e, - 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, - 0x31, 0x2e, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x23, - 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1d, 0x12, 0x1b, 0x2f, 0x76, 0x31, 0x2f, 0x72, 0x6f, 0x75, 0x6e, - 0x64, 0x2f, 0x70, 0x69, 0x6e, 0x67, 0x2f, 0x7b, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x5f, - 0x69, 0x64, 0x7d, 0x12, 0x64, 0x0a, 0x0d, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, 0x61, 0x79, - 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x1c, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, - 0x65, 0x61, 0x74, 0x65, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, - 0x74, 0x65, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x22, 0x16, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x10, 0x3a, 0x01, 0x2a, 0x22, 0x0b, 0x2f, 0x76, - 0x31, 0x2f, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x73, 0x0a, 0x0f, 0x43, 0x6f, 0x6d, - 0x70, 0x6c, 0x65, 0x74, 0x65, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x1e, 0x2e, 0x61, - 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x50, 0x61, - 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x61, - 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x50, 0x61, - 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1f, 0x82, - 0xd3, 0xe4, 0x93, 0x02, 0x19, 0x3a, 0x01, 0x2a, 0x22, 0x14, 0x2f, 0x76, 0x31, 0x2f, 0x70, 0x61, - 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x2f, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x57, - 0x0a, 0x08, 0x47, 0x65, 0x74, 0x52, 0x6f, 0x75, 0x6e, 0x64, 0x12, 0x17, 0x2e, 0x61, 0x72, 0x6b, - 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x6f, 0x75, 0x6e, 0x64, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, - 0x52, 0x6f, 0x75, 0x6e, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x18, 0x82, - 0xd3, 0xe4, 0x93, 0x02, 0x12, 0x12, 0x10, 0x2f, 0x76, 0x31, 0x2f, 0x72, 0x6f, 0x75, 0x6e, 0x64, - 0x2f, 0x7b, 0x74, 0x78, 0x69, 0x64, 0x7d, 0x12, 0x64, 0x0a, 0x0c, 0x47, 0x65, 0x74, 0x52, 0x6f, - 0x75, 0x6e, 0x64, 0x42, 0x79, 0x49, 0x64, 0x12, 0x1b, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, - 0x2e, 0x47, 0x65, 0x74, 0x52, 0x6f, 0x75, 0x6e, 0x64, 0x42, 0x79, 0x49, 0x64, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, - 0x74, 0x52, 0x6f, 0x75, 0x6e, 0x64, 0x42, 0x79, 0x49, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x22, 0x19, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x13, 0x12, 0x11, 0x2f, 0x76, 0x31, 0x2f, - 0x72, 0x6f, 0x75, 0x6e, 0x64, 0x2f, 0x69, 0x64, 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x12, 0x5d, 0x0a, - 0x09, 0x4c, 0x69, 0x73, 0x74, 0x56, 0x74, 0x78, 0x6f, 0x73, 0x12, 0x18, 0x2e, 0x61, 0x72, 0x6b, - 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x56, 0x74, 0x78, 0x6f, 0x73, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, - 0x73, 0x74, 0x56, 0x74, 0x78, 0x6f, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, - 0x1b, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x15, 0x12, 0x13, 0x2f, 0x76, 0x31, 0x2f, 0x76, 0x74, 0x78, - 0x6f, 0x73, 0x2f, 0x7b, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x7d, 0x12, 0x80, 0x01, 0x0a, - 0x15, 0x47, 0x65, 0x74, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, - 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x12, 0x24, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, - 0x47, 0x65, 0x74, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x53, - 0x74, 0x72, 0x65, 0x61, 0x6d, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x25, 0x2e, 0x61, - 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, - 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x22, 0x18, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x12, 0x12, 0x10, 0x2f, 0x76, 0x31, - 0x2f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x30, 0x01, 0x42, - 0x92, 0x01, 0x0a, 0x0a, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x42, 0x0c, - 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x3d, - 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x61, 0x72, 0x6b, 0x2d, 0x6e, - 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x2f, 0x61, 0x72, 0x6b, 0x2f, 0x61, 0x70, 0x69, 0x2d, 0x73, - 0x70, 0x65, 0x63, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x67, 0x65, 0x6e, - 0x2f, 0x61, 0x72, 0x6b, 0x2f, 0x76, 0x31, 0x3b, 0x61, 0x72, 0x6b, 0x76, 0x31, 0xa2, 0x02, 0x03, - 0x41, 0x58, 0x58, 0xaa, 0x02, 0x06, 0x41, 0x72, 0x6b, 0x2e, 0x56, 0x31, 0xca, 0x02, 0x06, 0x41, - 0x72, 0x6b, 0x5c, 0x56, 0x31, 0xe2, 0x02, 0x12, 0x41, 0x72, 0x6b, 0x5c, 0x56, 0x31, 0x5c, 0x47, - 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x07, 0x41, 0x72, 0x6b, - 0x3a, 0x3a, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x12, 0x29, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, + 0x65, 0x72, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x73, 0x46, 0x6f, 0x72, 0x4e, 0x65, 0x78, 0x74, 0x52, + 0x6f, 0x75, 0x6e, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, 0x61, 0x72, + 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x49, 0x6e, 0x70, + 0x75, 0x74, 0x73, 0x46, 0x6f, 0x72, 0x4e, 0x65, 0x78, 0x74, 0x52, 0x6f, 0x75, 0x6e, 0x64, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x23, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1d, 0x3a, + 0x01, 0x2a, 0x22, 0x18, 0x2f, 0x76, 0x31, 0x2f, 0x72, 0x6f, 0x75, 0x6e, 0x64, 0x2f, 0x72, 0x65, + 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x73, 0x12, 0x9c, 0x01, 0x0a, + 0x1b, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x73, + 0x46, 0x6f, 0x72, 0x4e, 0x65, 0x78, 0x74, 0x52, 0x6f, 0x75, 0x6e, 0x64, 0x12, 0x2a, 0x2e, 0x61, + 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x4f, 0x75, + 0x74, 0x70, 0x75, 0x74, 0x73, 0x46, 0x6f, 0x72, 0x4e, 0x65, 0x78, 0x74, 0x52, 0x6f, 0x75, 0x6e, + 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2b, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, + 0x31, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, + 0x73, 0x46, 0x6f, 0x72, 0x4e, 0x65, 0x78, 0x74, 0x52, 0x6f, 0x75, 0x6e, 0x64, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x24, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1e, 0x3a, 0x01, 0x2a, + 0x22, 0x19, 0x2f, 0x76, 0x31, 0x2f, 0x72, 0x6f, 0x75, 0x6e, 0x64, 0x2f, 0x72, 0x65, 0x67, 0x69, + 0x73, 0x74, 0x65, 0x72, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x73, 0x12, 0x7d, 0x0a, 0x10, 0x53, + 0x75, 0x62, 0x6d, 0x69, 0x74, 0x54, 0x72, 0x65, 0x65, 0x4e, 0x6f, 0x6e, 0x63, 0x65, 0x73, 0x12, + 0x1f, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x75, 0x62, 0x6d, 0x69, 0x74, 0x54, + 0x72, 0x65, 0x65, 0x4e, 0x6f, 0x6e, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x20, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x75, 0x62, 0x6d, 0x69, 0x74, + 0x54, 0x72, 0x65, 0x65, 0x4e, 0x6f, 0x6e, 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x22, 0x26, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x20, 0x3a, 0x01, 0x2a, 0x22, 0x1b, 0x2f, + 0x76, 0x31, 0x2f, 0x72, 0x6f, 0x75, 0x6e, 0x64, 0x2f, 0x74, 0x72, 0x65, 0x65, 0x2f, 0x73, 0x75, + 0x62, 0x6d, 0x69, 0x74, 0x4e, 0x6f, 0x6e, 0x63, 0x65, 0x73, 0x12, 0x8d, 0x01, 0x0a, 0x14, 0x53, + 0x75, 0x62, 0x6d, 0x69, 0x74, 0x54, 0x72, 0x65, 0x65, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, + 0x72, 0x65, 0x73, 0x12, 0x23, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x75, 0x62, + 0x6d, 0x69, 0x74, 0x54, 0x72, 0x65, 0x65, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, + 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, + 0x31, 0x2e, 0x53, 0x75, 0x62, 0x6d, 0x69, 0x74, 0x54, 0x72, 0x65, 0x65, 0x53, 0x69, 0x67, 0x6e, + 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x2a, + 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x24, 0x3a, 0x01, 0x2a, 0x22, 0x1f, 0x2f, 0x76, 0x31, 0x2f, 0x72, + 0x6f, 0x75, 0x6e, 0x64, 0x2f, 0x74, 0x72, 0x65, 0x65, 0x2f, 0x73, 0x75, 0x62, 0x6d, 0x69, 0x74, + 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x12, 0x8e, 0x01, 0x0a, 0x16, 0x53, + 0x75, 0x62, 0x6d, 0x69, 0x74, 0x53, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x46, 0x6f, 0x72, 0x66, 0x65, + 0x69, 0x74, 0x54, 0x78, 0x73, 0x12, 0x25, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x53, + 0x75, 0x62, 0x6d, 0x69, 0x74, 0x53, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x46, 0x6f, 0x72, 0x66, 0x65, + 0x69, 0x74, 0x54, 0x78, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x61, + 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x75, 0x62, 0x6d, 0x69, 0x74, 0x53, 0x69, 0x67, 0x6e, + 0x65, 0x64, 0x46, 0x6f, 0x72, 0x66, 0x65, 0x69, 0x74, 0x54, 0x78, 0x73, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x25, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1f, 0x3a, 0x01, 0x2a, 0x22, + 0x1a, 0x2f, 0x76, 0x31, 0x2f, 0x72, 0x6f, 0x75, 0x6e, 0x64, 0x2f, 0x73, 0x75, 0x62, 0x6d, 0x69, + 0x74, 0x46, 0x6f, 0x72, 0x66, 0x65, 0x69, 0x74, 0x54, 0x78, 0x73, 0x12, 0x6b, 0x0a, 0x0e, 0x47, + 0x65, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x12, 0x1d, 0x2e, + 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x53, + 0x74, 0x72, 0x65, 0x61, 0x6d, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x61, + 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x53, 0x74, + 0x72, 0x65, 0x61, 0x6d, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x18, 0x82, 0xd3, + 0xe4, 0x93, 0x02, 0x12, 0x12, 0x10, 0x2f, 0x76, 0x31, 0x2f, 0x72, 0x6f, 0x75, 0x6e, 0x64, 0x2f, + 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x30, 0x01, 0x12, 0x56, 0x0a, 0x04, 0x50, 0x69, 0x6e, 0x67, + 0x12, 0x13, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x50, + 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x23, 0x82, 0xd3, 0xe4, + 0x93, 0x02, 0x1d, 0x12, 0x1b, 0x2f, 0x76, 0x31, 0x2f, 0x72, 0x6f, 0x75, 0x6e, 0x64, 0x2f, 0x70, + 0x69, 0x6e, 0x67, 0x2f, 0x7b, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x7d, + 0x12, 0x64, 0x0a, 0x0d, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, + 0x74, 0x12, 0x1c, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, + 0x65, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x1d, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, + 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x16, + 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x10, 0x3a, 0x01, 0x2a, 0x22, 0x0b, 0x2f, 0x76, 0x31, 0x2f, 0x70, + 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x73, 0x0a, 0x0f, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, + 0x74, 0x65, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x1e, 0x2e, 0x61, 0x72, 0x6b, 0x2e, + 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x50, 0x61, 0x79, 0x6d, 0x65, + 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x61, 0x72, 0x6b, 0x2e, + 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x50, 0x61, 0x79, 0x6d, 0x65, + 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1f, 0x82, 0xd3, 0xe4, 0x93, + 0x02, 0x19, 0x3a, 0x01, 0x2a, 0x22, 0x14, 0x2f, 0x76, 0x31, 0x2f, 0x70, 0x61, 0x79, 0x6d, 0x65, + 0x6e, 0x74, 0x2f, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x57, 0x0a, 0x08, 0x47, + 0x65, 0x74, 0x52, 0x6f, 0x75, 0x6e, 0x64, 0x12, 0x17, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, + 0x2e, 0x47, 0x65, 0x74, 0x52, 0x6f, 0x75, 0x6e, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x18, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x6f, 0x75, + 0x6e, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x18, 0x82, 0xd3, 0xe4, 0x93, + 0x02, 0x12, 0x12, 0x10, 0x2f, 0x76, 0x31, 0x2f, 0x72, 0x6f, 0x75, 0x6e, 0x64, 0x2f, 0x7b, 0x74, + 0x78, 0x69, 0x64, 0x7d, 0x12, 0x64, 0x0a, 0x0c, 0x47, 0x65, 0x74, 0x52, 0x6f, 0x75, 0x6e, 0x64, + 0x42, 0x79, 0x49, 0x64, 0x12, 0x1b, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, + 0x74, 0x52, 0x6f, 0x75, 0x6e, 0x64, 0x42, 0x79, 0x49, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x1c, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x6f, + 0x75, 0x6e, 0x64, 0x42, 0x79, 0x49, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, + 0x19, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x13, 0x12, 0x11, 0x2f, 0x76, 0x31, 0x2f, 0x72, 0x6f, 0x75, + 0x6e, 0x64, 0x2f, 0x69, 0x64, 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x12, 0x5d, 0x0a, 0x09, 0x4c, 0x69, + 0x73, 0x74, 0x56, 0x74, 0x78, 0x6f, 0x73, 0x12, 0x18, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, + 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x56, 0x74, 0x78, 0x6f, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x19, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x56, + 0x74, 0x78, 0x6f, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1b, 0x82, 0xd3, + 0xe4, 0x93, 0x02, 0x15, 0x12, 0x13, 0x2f, 0x76, 0x31, 0x2f, 0x76, 0x74, 0x78, 0x6f, 0x73, 0x2f, + 0x7b, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x7d, 0x12, 0x80, 0x01, 0x0a, 0x15, 0x47, 0x65, + 0x74, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x53, 0x74, 0x72, + 0x65, 0x61, 0x6d, 0x12, 0x24, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, + 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x53, 0x74, 0x72, 0x65, + 0x61, 0x6d, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x25, 0x2e, 0x61, 0x72, 0x6b, 0x2e, + 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x73, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x22, 0x18, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x12, 0x12, 0x10, 0x2f, 0x76, 0x31, 0x2f, 0x74, 0x72, + 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x30, 0x01, 0x42, 0x92, 0x01, 0x0a, + 0x0a, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x42, 0x0c, 0x53, 0x65, 0x72, + 0x76, 0x69, 0x63, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x3d, 0x67, 0x69, 0x74, + 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x61, 0x72, 0x6b, 0x2d, 0x6e, 0x65, 0x74, 0x77, + 0x6f, 0x72, 0x6b, 0x2f, 0x61, 0x72, 0x6b, 0x2f, 0x61, 0x70, 0x69, 0x2d, 0x73, 0x70, 0x65, 0x63, + 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x61, 0x72, + 0x6b, 0x2f, 0x76, 0x31, 0x3b, 0x61, 0x72, 0x6b, 0x76, 0x31, 0xa2, 0x02, 0x03, 0x41, 0x58, 0x58, + 0xaa, 0x02, 0x06, 0x41, 0x72, 0x6b, 0x2e, 0x56, 0x31, 0xca, 0x02, 0x06, 0x41, 0x72, 0x6b, 0x5c, + 0x56, 0x31, 0xe2, 0x02, 0x12, 0x41, 0x72, 0x6b, 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, + 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x07, 0x41, 0x72, 0x6b, 0x3a, 0x3a, 0x56, + 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/client/main.go b/client/main.go index 41c55de..fdc2eb1 100644 --- a/client/main.go +++ b/client/main.go @@ -39,7 +39,7 @@ func main() { &configCommand, &dumpCommand, &receiveCommand, - &claimCmd, + &settleCmd, &sendCommand, &balanceCommand, &redeemCommand, @@ -160,11 +160,11 @@ var ( return receive(ctx) }, } - claimCmd = cli.Command{ - Name: "claim", - Usage: "Claim onboarding funds or pending payments", + settleCmd = cli.Command{ + Name: "settle", + Usage: "Settle onboarding funds or oor payments", Action: func(ctx *cli.Context) error { - return claim(ctx) + return settle(ctx) }, Flags: []cli.Flag{passwordFlag}, } @@ -266,7 +266,7 @@ func receive(ctx *cli.Context) error { }) } -func claim(ctx *cli.Context) error { +func settle(ctx *cli.Context) error { password, err := readPassword(ctx) if err != nil { return err @@ -275,7 +275,7 @@ func claim(ctx *cli.Context) error { return err } - txID, err := arkSdkClient.Claim(ctx.Context) + txID, err := arkSdkClient.Settle(ctx.Context) if err != nil { return err } diff --git a/common/bitcointree/vtxo.go b/common/bitcointree/vtxo.go index f57d1ad..a8bf770 100644 --- a/common/bitcointree/vtxo.go +++ b/common/bitcointree/vtxo.go @@ -15,20 +15,17 @@ import ( type VtxoScript common.VtxoScript[bitcoinTapTree] func ParseVtxoScript(desc string) (VtxoScript, error) { - v := &DefaultVtxoScript{} - // TODO add other type - err := v.FromDescriptor(desc) - if err != nil { - v := &ReversibleVtxoScript{} - err = v.FromDescriptor(desc) - if err != nil { - return nil, fmt.Errorf("invalid vtxo descriptor: %s", desc) - } - - return v, nil + types := []VtxoScript{ + &DefaultVtxoScript{}, } - return v, nil + for _, v := range types { + if err := v.FromDescriptor(desc); err == nil { + return v, nil + } + } + + return nil, fmt.Errorf("invalid vtxo descriptor: %s", desc) } /* @@ -101,94 +98,6 @@ func (v *DefaultVtxoScript) TapTree() (*secp256k1.PublicKey, bitcoinTapTree, err return taprootKey, bitcoinTapTree{tapTree}, nil } -/* -* ReversibleVtxoScript allows sender of the VTXO to revert the transaction -* unilateral exit is in favor of the sender -* - Owner and ASP (forfeit owner) -* - Sender and ASP (forfeit sender) -* - Sender after t (unilateral exit) - */ -type ReversibleVtxoScript struct { - Asp *secp256k1.PublicKey - Sender *secp256k1.PublicKey - Owner *secp256k1.PublicKey - ExitDelay uint -} - -func (v *ReversibleVtxoScript) ToDescriptor() string { - owner := hex.EncodeToString(schnorr.SerializePubKey(v.Owner)) - sender := hex.EncodeToString(schnorr.SerializePubKey(v.Sender)) - asp := hex.EncodeToString(schnorr.SerializePubKey(v.Asp)) - - return fmt.Sprintf( - descriptor.ReversibleVtxoScriptTemplate, - hex.EncodeToString(UnspendableKey().SerializeCompressed()), - sender, - asp, - v.ExitDelay, - sender, - owner, - asp, - ) -} - -func (v *ReversibleVtxoScript) FromDescriptor(desc string) error { - owner, sender, asp, exitDelay, err := descriptor.ParseReversibleVtxoDescriptor(desc) - if err != nil { - return err - } - - v.Owner = owner - v.Sender = sender - v.Asp = asp - v.ExitDelay = exitDelay - return nil -} - -func (v *ReversibleVtxoScript) TapTree() (*secp256k1.PublicKey, bitcoinTapTree, error) { - redeemClosure := &CSVSigClosure{ - Pubkey: v.Sender, - Seconds: v.ExitDelay, - } - - redeemLeaf, err := redeemClosure.Leaf() - if err != nil { - return nil, bitcoinTapTree{}, err - } - - forfeitClosure := &MultisigClosure{ - Pubkey: v.Owner, - AspPubkey: v.Asp, - } - - forfeitLeaf, err := forfeitClosure.Leaf() - if err != nil { - return nil, bitcoinTapTree{}, err - } - - reverseForfeitClosure := &MultisigClosure{ - Pubkey: v.Sender, - AspPubkey: v.Asp, - } - - reverseForfeitLeaf, err := reverseForfeitClosure.Leaf() - if err != nil { - return nil, bitcoinTapTree{}, err - } - - tapTree := txscript.AssembleTaprootScriptTree( - *redeemLeaf, *forfeitLeaf, *reverseForfeitLeaf, - ) - - root := tapTree.RootNode.TapHash() - taprootKey := txscript.ComputeTaprootOutputKey( - UnspendableKey(), - root[:], - ) - - return taprootKey, bitcoinTapTree{tapTree}, nil -} - // bitcoinTapTree is a wrapper around txscript.IndexedTapScriptTree to implement the common.TaprootTree interface type bitcoinTapTree struct { *txscript.IndexedTapScriptTree diff --git a/common/bitcointree/vtxo_test.go b/common/bitcointree/vtxo_test.go index 927824e..271b903 100644 --- a/common/bitcointree/vtxo_test.go +++ b/common/bitcointree/vtxo_test.go @@ -19,12 +19,8 @@ func TestParseDescriptor(t *testing.T) { aliceKey, err := secp256k1.GeneratePrivateKey() require.NoError(t, err) - bobKey, err := secp256k1.GeneratePrivateKey() - require.NoError(t, err) - aspPubKey := hex.EncodeToString(schnorr.SerializePubKey(aspKey.PubKey())) alicePubKey := hex.EncodeToString(schnorr.SerializePubKey(aliceKey.PubKey())) - bobPubKey := hex.EncodeToString(schnorr.SerializePubKey(bobKey.PubKey())) unspendableKey := hex.EncodeToString(bitcointree.UnspendableKey().SerializeCompressed()) @@ -44,24 +40,4 @@ func TestParseDescriptor(t *testing.T) { require.Equal(t, defaultScriptDescriptor, vtxo.ToDescriptor()) require.Equal(t, alicePubKey, hex.EncodeToString(schnorr.SerializePubKey(vtxo.(*bitcointree.DefaultVtxoScript).Owner))) require.Equal(t, aspPubKey, hex.EncodeToString(schnorr.SerializePubKey(vtxo.(*bitcointree.DefaultVtxoScript).Asp))) - - reversibleScriptDescriptor := fmt.Sprintf( - descriptor.ReversibleVtxoScriptTemplate, - unspendableKey, - alicePubKey, - aspPubKey, - 512, - alicePubKey, - bobPubKey, - aspPubKey, - ) - - vtxo, err = bitcointree.ParseVtxoScript(reversibleScriptDescriptor) - require.NoError(t, err) - - require.IsType(t, &bitcointree.ReversibleVtxoScript{}, vtxo) - require.Equal(t, reversibleScriptDescriptor, vtxo.ToDescriptor()) - require.Equal(t, alicePubKey, hex.EncodeToString(schnorr.SerializePubKey(vtxo.(*bitcointree.ReversibleVtxoScript).Sender))) - require.Equal(t, bobPubKey, hex.EncodeToString(schnorr.SerializePubKey(vtxo.(*bitcointree.ReversibleVtxoScript).Owner))) - require.Equal(t, aspPubKey, hex.EncodeToString(schnorr.SerializePubKey(vtxo.(*bitcointree.ReversibleVtxoScript).Asp))) } diff --git a/common/descriptor/ark.go b/common/descriptor/ark.go index 2b1da0d..f2805cd 100644 --- a/common/descriptor/ark.go +++ b/common/descriptor/ark.go @@ -11,91 +11,6 @@ import ( // tr(unspendable, { and(pk(user), pk(asp)), and(older(timeout), pk(user)) }) const DefaultVtxoDescriptorTemplate = "tr(%s,{ and(pk(%s), pk(%s)), and(older(%d), pk(%s)) })" -// tr(unspendable, { { and(pk(sender), pk(asp)), and(older(timeout), pk(sender)) }, and(pk(receiver), pk(asp)) }) -const ReversibleVtxoScriptTemplate = "tr(%s,{ { and(pk(%s), pk(%s)), and(older(%d), pk(%s)) }, and(pk(%s), pk(%s)) })" - -func ParseReversibleVtxoDescriptor( - descriptor string, -) (user, sender, asp *secp256k1.PublicKey, timeout uint, err error) { - desc, err := ParseTaprootDescriptor(descriptor) - if err != nil { - return nil, nil, nil, 0, err - } - - if len(desc.ScriptTree) != 3 { - return nil, nil, nil, 0, errors.New("not a reversible vtxo script descriptor") - } - - for _, leaf := range desc.ScriptTree { - if andLeaf, ok := leaf.(*And); ok { - if first, ok := andLeaf.First.(*PK); ok { - if second, ok := andLeaf.Second.(*PK); ok { - keyBytes, err := hex.DecodeString(first.Key.Hex) - if err != nil { - return nil, nil, nil, 0, err - } - if sender == nil { - sender, err = schnorr.ParsePubKey(keyBytes) - if err != nil { - return nil, nil, nil, 0, err - } - } else { - user, err = schnorr.ParsePubKey(keyBytes) - if err != nil { - return nil, nil, nil, 0, err - } - } - - if asp == nil { - keyBytes, err = hex.DecodeString(second.Key.Hex) - if err != nil { - return nil, nil, nil, 0, err - } - - asp, err = schnorr.ParsePubKey(keyBytes) - if err != nil { - return nil, nil, nil, 0, err - } - } - } - } - - if first, ok := andLeaf.First.(*Older); ok { - if second, ok := andLeaf.Second.(*PK); ok { - timeout = first.Timeout - keyBytes, err := hex.DecodeString(second.Key.Hex) - if err != nil { - return nil, nil, nil, 0, err - } - - sender, err = schnorr.ParsePubKey(keyBytes) - if err != nil { - return nil, nil, nil, 0, err - } - } - } - } - } - - if user == nil { - return nil, nil, nil, 0, errors.New("descriptor is invalid") - } - - if asp == nil { - return nil, nil, nil, 0, errors.New("descriptor is invalid") - } - - if timeout == 0 { - return nil, nil, nil, 0, errors.New("descriptor is invalid") - } - - if sender == nil { - return nil, nil, nil, 0, errors.New("descriptor is invalid") - } - - return -} - func ParseDefaultVtxoDescriptor( descriptor string, ) (user, asp *secp256k1.PublicKey, timeout uint, err error) { diff --git a/pkg/client-sdk/ark_sdk.go b/pkg/client-sdk/ark_sdk.go index 87eca1b..5a0c415 100644 --- a/pkg/client-sdk/ark_sdk.go +++ b/pkg/client-sdk/ark_sdk.go @@ -25,7 +25,7 @@ type ArkClient interface { ctx context.Context, addr string, amount uint64, withExpiryCoinselect bool, ) (string, error) SendAsync(ctx context.Context, withExpiryCoinselect bool, receivers []Receiver) (string, error) - Claim(ctx context.Context) (string, error) + Settle(ctx context.Context) (string, error) ListVtxos(ctx context.Context) (spendable, spent []client.Vtxo, err error) Dump(ctx context.Context) (seed string, err error) GetTransactionHistory(ctx context.Context) ([]types.Transaction, error) diff --git a/pkg/client-sdk/client.go b/pkg/client-sdk/client.go index 92ce9b5..d03b89b 100644 --- a/pkg/client-sdk/client.go +++ b/pkg/client-sdk/client.go @@ -270,6 +270,27 @@ func (a *arkClient) ping( return ticker.Stop } +func (a *arkClient) ListVtxos( + ctx context.Context, +) (spendableVtxos, spentVtxos []client.Vtxo, err error) { + offchainAddrs, _, _, err := a.wallet.GetAddresses(ctx) + if err != nil { + return + } + + for _, addr := range offchainAddrs { + spendable, spent, err := a.client.ListVtxos(ctx, addr.Address) + if err != nil { + return nil, nil, err + } + + spendableVtxos = append(spendableVtxos, spendable...) + spentVtxos = append(spentVtxos, spent...) + } + + return +} + func getClient( supportedClients utils.SupportedType[utils.ClientFactory], clientType, aspUrl string, ) (client.ASPClient, error) { @@ -329,3 +350,15 @@ func getWalletStore(storeType, datadir string) (walletstore.WalletStore, error) func getCreatedAtFromExpiry(roundLifetime int64, expiry time.Time) time.Time { return expiry.Add(-time.Duration(roundLifetime) * time.Second) } + +func filterByOutpoints(vtxos []client.Vtxo, outpoints []client.Outpoint) []client.Vtxo { + filtered := make([]client.Vtxo, 0, len(vtxos)) + for _, vtxo := range vtxos { + for _, outpoint := range outpoints { + if vtxo.Outpoint.Equals(outpoint) { + filtered = append(filtered, vtxo) + } + } + } + return filtered +} diff --git a/pkg/client-sdk/client/client.go b/pkg/client-sdk/client/client.go index d832ef7..e29175a 100644 --- a/pkg/client-sdk/client/client.go +++ b/pkg/client-sdk/client/client.go @@ -78,6 +78,10 @@ type Outpoint struct { VOut uint32 } +func (o Outpoint) Equals(other Outpoint) bool { + return o.Txid == other.Txid && o.VOut == other.VOut +} + type Input struct { Outpoint Descriptor string @@ -95,7 +99,7 @@ type Vtxo struct { RoundTxid string ExpiresAt *time.Time RedeemTx string - Pending bool + IsOOR bool SpentBy string } diff --git a/pkg/client-sdk/client/grpc/client.go b/pkg/client-sdk/client/grpc/client.go index 30e352f..9250702 100644 --- a/pkg/client-sdk/client/grpc/client.go +++ b/pkg/client-sdk/client/grpc/client.go @@ -359,7 +359,7 @@ func (c *grpcClient) GetTransactionsStream( Round: &client.RoundTransaction{ Txid: tx.Round.Txid, SpentVtxos: outpointsFromProto(tx.Round.SpentVtxos), - SpendableVtxos: vtxosFromProto(tx.Round.SpendableVtxos), + SpendableVtxos: vtxos(tx.Round.SpendableVtxos).toVtxos(), ClaimedBoardingUtxos: outpointsFromProto(tx.Round.ClaimedBoardingUtxos), }, } @@ -368,7 +368,7 @@ func (c *grpcClient) GetTransactionsStream( Redeem: &client.RedeemTransaction{ Txid: tx.Redeem.Txid, SpentVtxos: outpointsFromProto(tx.Redeem.SpentVtxos), - SpendableVtxos: vtxosFromProto(tx.Redeem.SpendableVtxos), + SpendableVtxos: vtxos(tx.Redeem.SpendableVtxos).toVtxos(), }, } } @@ -394,24 +394,3 @@ func outpointsFromProto(protoOutpoints []*arkv1.Outpoint) []client.Outpoint { } return outpoints } - -func vtxosFromProto(protoVtxos []*arkv1.Vtxo) []client.Vtxo { - vtxos := make([]client.Vtxo, len(protoVtxos)) - for i, v := range protoVtxos { - expiresAt := time.Unix(v.ExpireAt, 0) - vtxos[i] = client.Vtxo{ - Outpoint: client.Outpoint{ - Txid: v.Outpoint.Txid, - VOut: v.Outpoint.Vout, - }, - Pubkey: v.Pubkey, - Amount: v.Amount, - RoundTxid: v.RoundTxid, - ExpiresAt: &expiresAt, - RedeemTx: v.RedeemTx, - Pending: v.Pending, - SpentBy: v.SpentBy, - } - } - return vtxos -} diff --git a/pkg/client-sdk/client/grpc/types.go b/pkg/client-sdk/client/grpc/types.go index 4b13dae..6070af7 100644 --- a/pkg/client-sdk/client/grpc/types.go +++ b/pkg/client-sdk/client/grpc/types.go @@ -125,7 +125,7 @@ func (v vtxo) toVtxo() client.Vtxo { Amount: v.GetAmount(), RoundTxid: v.GetRoundTxid(), ExpiresAt: expiresAt, - Pending: v.GetPending(), + IsOOR: v.GetIsOor(), RedeemTx: v.GetRedeemTx(), SpentBy: v.GetSpentBy(), Pubkey: v.GetPubkey(), diff --git a/pkg/client-sdk/client/rest/client.go b/pkg/client-sdk/client/rest/client.go index 73458c4..ec8a6f8 100644 --- a/pkg/client-sdk/client/rest/client.go +++ b/pkg/client-sdk/client/rest/client.go @@ -499,7 +499,7 @@ func (a *restClient) ListVtxos( Amount: uint64(amount), RoundTxid: v.RoundTxid, ExpiresAt: expiresAt, - Pending: v.Pending, + IsOOR: v.IsOor, RedeemTx: v.RedeemTx, SpentBy: v.SpentBy, Pubkey: v.Pubkey, @@ -701,7 +701,7 @@ func vtxosFromRest(restVtxos []*models.V1Vtxo) []client.Vtxo { RoundTxid: v.RoundTxid, ExpiresAt: expiresAt, RedeemTx: v.RedeemTx, - Pending: v.Pending, + IsOOR: v.IsOor, SpentBy: v.SpentBy, } } diff --git a/pkg/client-sdk/client/rest/service/models/v1_vtxo.go b/pkg/client-sdk/client/rest/service/models/v1_vtxo.go index 5bca48d..5ae090e 100644 --- a/pkg/client-sdk/client/rest/service/models/v1_vtxo.go +++ b/pkg/client-sdk/client/rest/service/models/v1_vtxo.go @@ -24,12 +24,12 @@ type V1Vtxo struct { // expire at ExpireAt string `json:"expireAt,omitempty"` + // is oor + IsOor bool `json:"isOor,omitempty"` + // outpoint Outpoint *V1Outpoint `json:"outpoint,omitempty"` - // pending - Pending bool `json:"pending,omitempty"` - // pubkey Pubkey string `json:"pubkey,omitempty"` diff --git a/pkg/client-sdk/client_test.go b/pkg/client-sdk/client_test.go index b615bb5..aa889fe 100644 --- a/pkg/client-sdk/client_test.go +++ b/pkg/client-sdk/client_test.go @@ -33,7 +33,6 @@ func TestVtxosToTxs(t *testing.T) { }, Amount: 1000, Type: sdktypes.TxSent, - IsPending: false, CreatedAt: time.Unix(1726054898, 0), }, }, @@ -48,7 +47,6 @@ func TestVtxosToTxs(t *testing.T) { }, Amount: 1000, Type: sdktypes.TxReceived, - IsPending: true, CreatedAt: time.Unix(1726054898, 0), }, { @@ -57,7 +55,6 @@ func TestVtxosToTxs(t *testing.T) { }, Amount: 2000, Type: sdktypes.TxReceived, - IsPending: true, CreatedAt: time.Unix(1726486359, 0), }, }, @@ -72,7 +69,6 @@ func TestVtxosToTxs(t *testing.T) { }, Amount: 1000, Type: sdktypes.TxReceived, - IsPending: false, CreatedAt: time.Unix(1726054898, 0), }, { @@ -81,7 +77,6 @@ func TestVtxosToTxs(t *testing.T) { }, Amount: 2000, Type: sdktypes.TxReceived, - IsPending: false, CreatedAt: time.Unix(1726486359, 0), }, }, @@ -96,7 +91,6 @@ func TestVtxosToTxs(t *testing.T) { }, Amount: 1000, Type: sdktypes.TxReceived, - IsPending: false, CreatedAt: time.Unix(1726054898, 0), }, { @@ -105,7 +99,6 @@ func TestVtxosToTxs(t *testing.T) { }, Amount: 2000, Type: sdktypes.TxReceived, - IsPending: false, CreatedAt: time.Unix(1726486359, 0), }, { @@ -114,7 +107,6 @@ func TestVtxosToTxs(t *testing.T) { }, Amount: 2100, Type: sdktypes.TxSent, - IsPending: false, CreatedAt: time.Unix(1726503865, 0), }, }, @@ -138,7 +130,6 @@ func TestVtxosToTxs(t *testing.T) { require.Equal(t, wantTx.RedeemTxid, gotTx.RedeemTxid) require.Equal(t, int(wantTx.Amount), int(gotTx.Amount)) require.Equal(t, wantTx.Type, gotTx.Type) - require.Equal(t, wantTx.IsPending, gotTx.IsPending) } }) } @@ -161,16 +152,12 @@ func loadFixtures(jsonStr string) (vtxos, map[string]struct{}, error) { Address string `json:"address"` Amount string `json:"amount"` } `json:"receiver"` - Spent bool `json:"spent"` - PoolTxid string `json:"poolTxid"` - SpentBy string `json:"spentBy"` - ExpireAt string `json:"expireAt"` - Swept bool `json:"swept"` - Pending bool `json:"pending"` - PendingData struct { - RedeemTx string `json:"redeemTx"` - UnconditionalForfeitTxs []string `json:"unconditionalForfeitTxs"` - } `json:"pendingData"` + Spent bool `json:"spent"` + PoolTxid string `json:"poolTxid"` + SpentBy string `json:"spentBy"` + ExpireAt string `json:"expireAt"` + Swept bool `json:"swept"` + RedeemTx string `json:"redeemTx"` } `json:"spendableVtxos"` SpentVtxos []struct { Outpoint struct { @@ -181,16 +168,12 @@ func loadFixtures(jsonStr string) (vtxos, map[string]struct{}, error) { Address string `json:"address"` Amount string `json:"amount"` } `json:"receiver"` - Spent bool `json:"spent"` - PoolTxid string `json:"poolTxid"` - SpentBy string `json:"spentBy"` - ExpireAt string `json:"expireAt"` - Swept bool `json:"swept"` - Pending bool `json:"pending"` - PendingData struct { - RedeemTx string `json:"redeemTx"` - UnconditionalForfeitTxs []string `json:"unconditionalForfeitTxs"` - } `json:"pendingData"` + Spent bool `json:"spent"` + PoolTxid string `json:"poolTxid"` + SpentBy string `json:"spentBy"` + ExpireAt string `json:"expireAt"` + Swept bool `json:"swept"` + RedeemTx string `json:"redeemTx"` } `json:"spentVtxos"` } @@ -216,8 +199,7 @@ func loadFixtures(jsonStr string) (vtxos, map[string]struct{}, error) { Amount: amount, RoundTxid: vtxo.PoolTxid, ExpiresAt: &expireAt, - RedeemTx: vtxo.PendingData.RedeemTx, - Pending: vtxo.Pending, + RedeemTx: vtxo.RedeemTx, SpentBy: vtxo.SpentBy, } } @@ -240,8 +222,7 @@ func loadFixtures(jsonStr string) (vtxos, map[string]struct{}, error) { Amount: amount, RoundTxid: vtxo.PoolTxid, ExpiresAt: &expireAt, - RedeemTx: vtxo.PendingData.RedeemTx, - Pending: vtxo.Pending, + RedeemTx: vtxo.RedeemTx, SpentBy: vtxo.SpentBy, } } @@ -301,9 +282,7 @@ var ( "poolTxid": "377fa2fbd27c82bdbc095478384c88b6c75432c0ef464189e49c965194446cdf", "spentBy": "", "expireAt": "1726054928", - "swept": false, - "pending": false, - "pendingData": null + "swept": false } ], "spentVtxos": [] @@ -329,13 +308,7 @@ var ( "spentBy": "", "expireAt": "1726054928", "swept": false, - "pending": false, - "pendingData": { - "redeemTx": "cHNidP8BAIkCAAAAAWwxXUjG5tidFA0LmUljX//jwW6xWaS6HKyRCw5StsxpAAAAAAD/////AugDAAAAAAAAIlEgt2eR8LtqTP7yUcQtSydeGrRiHnVmHHnZwYjdC23G7MZwSQAAAAAAACJRIKfUzf/o9h+r0v9y4nmyOt9qO8EkDumQPQZGTbEv8fSFAAAAAAABASsgTgAAAAAAACJRIKfUzf/o9h+r0v9y4nmyOt9qO8EkDumQPQZGTbEv8fSFIgYDp9sz9Cto7FhF+C929Y/jJw1zkRWxq6NURd9kX05mShcYAAAAAFYAAIAAAACAAQAAgAAAAAAAAAAAQRSvBIBey3T0IV353FkuGLMmMJFpqHTrliIsJwJsfJkzq7J0B8bQ0j9842h5lUfOWcbj2TeoFx6OCpgoHIqWIBhHQAFqkBLiRmP3AZ8MS77s1QIWZswMV3L72D9gN0f0MbD6XHkmzZeC1clF3uzxr+13wsF0vcFe29Zl3e2gAhMNGYVCFcFQkpt0waBJVLeLS2A16XpeB4paDyjsltVHv+6azoA6wKRtST8P7teUpSF4DAEbfJj5OIXITx5QGbZns/AtxqGyRSCn2zP0K2jsWEX4L3b1j+MnDXORFbGro1RF32RfTmZKF60grwSAXst09CFd+dxZLhizJjCRaah065YiLCcCbHyZM6uswCEWp9sz9Cto7FhF+C929Y/jJw1zkRWxq6NURd9kX05mShc5AbJ0B8bQ0j9842h5lUfOWcbj2TeoFx6OCpgoHIqWIBhHAAAAAFYAAIAAAACAAQAAgAAAAAAAAAAAARcgUJKbdMGgSVS3i0tgNel6XgeKWg8o7JbVR7/ums6AOsAAAAA=", - "unconditionalForfeitTxs": [ - "cHNidP8BAFICAAAAAWwxXUjG5tidFA0LmUljX//jwW6xWaS6HKyRCw5StsxpAAAAAAD/////AVhNAAAAAAAAFgAUSU38/3Mzx5BdILG4oUO+JoHcoT8AAAAAAAEBKyBOAAAAAAAAIlEgp9TN/+j2H6vS/3LiebI632o7wSQO6ZA9BkZNsS/x9IVBFK8EgF7LdPQhXfncWS4YsyYwkWmodOuWIiwnAmx8mTOrsnQHxtDSP3zjaHmVR85ZxuPZN6gXHo4KmCgcipYgGEdAjH8Mg1Z3GdjGzp78Mg2xq1fop9KDfeji+xoyMgYS7q0Nl0AGOAaNzkDRW4cNcefll5jZC2i3nfygKdXsUsR+LEIVwVCSm3TBoElUt4tLYDXpel4HiloPKOyW1Ue/7prOgDrApG1JPw/u15SlIXgMARt8mPk4hchPHlAZtmez8C3GobJFIKfbM/QraOxYRfgvdvWP4ycNc5EVsaujVEXfZF9OZkoXrSCvBIBey3T0IV353FkuGLMmMJFpqHTrliIsJwJsfJkzq6zAARcgUJKbdMGgSVS3i0tgNel6XgeKWg8o7JbVR7/ums6AOsAAAA==" - ] - } + "redeemTx": "cHNidP8BAIkCAAAAAWwxXUjG5tidFA0LmUljX//jwW6xWaS6HKyRCw5StsxpAAAAAAD/////AugDAAAAAAAAIlEgt2eR8LtqTP7yUcQtSydeGrRiHnVmHHnZwYjdC23G7MZwSQAAAAAAACJRIKfUzf/o9h+r0v9y4nmyOt9qO8EkDumQPQZGTbEv8fSFAAAAAAABASsgTgAAAAAAACJRIKfUzf/o9h+r0v9y4nmyOt9qO8EkDumQPQZGTbEv8fSFIgYDp9sz9Cto7FhF+C929Y/jJw1zkRWxq6NURd9kX05mShcYAAAAAFYAAIAAAACAAQAAgAAAAAAAAAAAQRSvBIBey3T0IV353FkuGLMmMJFpqHTrliIsJwJsfJkzq7J0B8bQ0j9842h5lUfOWcbj2TeoFx6OCpgoHIqWIBhHQAFqkBLiRmP3AZ8MS77s1QIWZswMV3L72D9gN0f0MbD6XHkmzZeC1clF3uzxr+13wsF0vcFe29Zl3e2gAhMNGYVCFcFQkpt0waBJVLeLS2A16XpeB4paDyjsltVHv+6azoA6wKRtST8P7teUpSF4DAEbfJj5OIXITx5QGbZns/AtxqGyRSCn2zP0K2jsWEX4L3b1j+MnDXORFbGro1RF32RfTmZKF60grwSAXst09CFd+dxZLhizJjCRaah065YiLCcCbHyZM6uswCEWp9sz9Cto7FhF+C929Y/jJw1zkRWxq6NURd9kX05mShc5AbJ0B8bQ0j9842h5lUfOWcbj2TeoFx6OCpgoHIqWIBhHAAAAAFYAAIAAAACAAQAAgAAAAAAAAAAAARcgUJKbdMGgSVS3i0tgNel6XgeKWg8o7JbVR7/ums6AOsAAAAA=" } ], "spentVtxos": [ @@ -353,8 +326,7 @@ var ( "spentBy": "94fa598302f17f00c8881e742ec0ce2f8c8d16f3d54fe6ba0fb7d13a493d84ad", "expireAt": "1726054928", "swept": false, - "pending": false, - "pendingData": null + "redeemTx": "" } ] }` @@ -376,13 +348,7 @@ var ( "spentBy": "", "expireAt": "1726054928", "swept": false, - "pending": true, - "pendingData": { - "redeemTx": "cHNidP8BAIkCAAAAAWwxXUjG5tidFA0LmUljX//jwW6xWaS6HKyRCw5StsxpAAAAAAD/////AugDAAAAAAAAIlEgt2eR8LtqTP7yUcQtSydeGrRiHnVmHHnZwYjdC23G7MZwSQAAAAAAACJRIKfUzf/o9h+r0v9y4nmyOt9qO8EkDumQPQZGTbEv8fSFAAAAAAABASsgTgAAAAAAACJRIKfUzf/o9h+r0v9y4nmyOt9qO8EkDumQPQZGTbEv8fSFIgYDp9sz9Cto7FhF+C929Y/jJw1zkRWxq6NURd9kX05mShcYAAAAAFYAAIAAAACAAQAAgAAAAAAAAAAAQRSvBIBey3T0IV353FkuGLMmMJFpqHTrliIsJwJsfJkzq7J0B8bQ0j9842h5lUfOWcbj2TeoFx6OCpgoHIqWIBhHQAFqkBLiRmP3AZ8MS77s1QIWZswMV3L72D9gN0f0MbD6XHkmzZeC1clF3uzxr+13wsF0vcFe29Zl3e2gAhMNGYVCFcFQkpt0waBJVLeLS2A16XpeB4paDyjsltVHv+6azoA6wKRtST8P7teUpSF4DAEbfJj5OIXITx5QGbZns/AtxqGyRSCn2zP0K2jsWEX4L3b1j+MnDXORFbGro1RF32RfTmZKF60grwSAXst09CFd+dxZLhizJjCRaah065YiLCcCbHyZM6uswCEWp9sz9Cto7FhF+C929Y/jJw1zkRWxq6NURd9kX05mShc5AbJ0B8bQ0j9842h5lUfOWcbj2TeoFx6OCpgoHIqWIBhHAAAAAFYAAIAAAACAAQAAgAAAAAAAAAAAARcgUJKbdMGgSVS3i0tgNel6XgeKWg8o7JbVR7/ums6AOsAAAAA=", - "unconditionalForfeitTxs": [ - "cHNidP8BAFICAAAAAWwxXUjG5tidFA0LmUljX//jwW6xWaS6HKyRCw5StsxpAAAAAAD/////AVhNAAAAAAAAFgAUSU38/3Mzx5BdILG4oUO+JoHcoT8AAAAAAAEBKyBOAAAAAAAAIlEgp9TN/+j2H6vS/3LiebI632o7wSQO6ZA9BkZNsS/x9IVBFK8EgF7LdPQhXfncWS4YsyYwkWmodOuWIiwnAmx8mTOrsnQHxtDSP3zjaHmVR85ZxuPZN6gXHo4KmCgcipYgGEdAjH8Mg1Z3GdjGzp78Mg2xq1fop9KDfeji+xoyMgYS7q0Nl0AGOAaNzkDRW4cNcefll5jZC2i3nfygKdXsUsR+LEIVwVCSm3TBoElUt4tLYDXpel4HiloPKOyW1Ue/7prOgDrApG1JPw/u15SlIXgMARt8mPk4hchPHlAZtmez8C3GobJFIKfbM/QraOxYRfgvdvWP4ycNc5EVsaujVEXfZF9OZkoXrSCvBIBey3T0IV353FkuGLMmMJFpqHTrliIsJwJsfJkzq6zAARcgUJKbdMGgSVS3i0tgNel6XgeKWg8o7JbVR7/ums6AOsAAAA==" - ] - } + "redeemTx": "cHNidP8BAIkCAAAAAWwxXUjG5tidFA0LmUljX//jwW6xWaS6HKyRCw5StsxpAAAAAAD/////AugDAAAAAAAAIlEgt2eR8LtqTP7yUcQtSydeGrRiHnVmHHnZwYjdC23G7MZwSQAAAAAAACJRIKfUzf/o9h+r0v9y4nmyOt9qO8EkDumQPQZGTbEv8fSFAAAAAAABASsgTgAAAAAAACJRIKfUzf/o9h+r0v9y4nmyOt9qO8EkDumQPQZGTbEv8fSFIgYDp9sz9Cto7FhF+C929Y/jJw1zkRWxq6NURd9kX05mShcYAAAAAFYAAIAAAACAAQAAgAAAAAAAAAAAQRSvBIBey3T0IV353FkuGLMmMJFpqHTrliIsJwJsfJkzq7J0B8bQ0j9842h5lUfOWcbj2TeoFx6OCpgoHIqWIBhHQAFqkBLiRmP3AZ8MS77s1QIWZswMV3L72D9gN0f0MbD6XHkmzZeC1clF3uzxr+13wsF0vcFe29Zl3e2gAhMNGYVCFcFQkpt0waBJVLeLS2A16XpeB4paDyjsltVHv+6azoA6wKRtST8P7teUpSF4DAEbfJj5OIXITx5QGbZns/AtxqGyRSCn2zP0K2jsWEX4L3b1j+MnDXORFbGro1RF32RfTmZKF60grwSAXst09CFd+dxZLhizJjCRaah065YiLCcCbHyZM6uswCEWp9sz9Cto7FhF+C929Y/jJw1zkRWxq6NURd9kX05mShc5AbJ0B8bQ0j9842h5lUfOWcbj2TeoFx6OCpgoHIqWIBhHAAAAAFYAAIAAAACAAQAAgAAAAAAAAAAAARcgUJKbdMGgSVS3i0tgNel6XgeKWg8o7JbVR7/ums6AOsAAAAA=" }, { "outpoint": { @@ -398,13 +364,7 @@ var ( "spentBy": "", "expireAt": "1726486389", "swept": false, - "pending": true, - "pendingData": { - "redeemTx": "cHNidP8BAIkCAAAAARH6LJRGP/pFIkD/o5bBp8fXAhjl8yjfN7MhJsxdt5lrAQAAAAD/////AtAHAAAAAAAAIlEguuBh3KQUVZp+NHV2sixQ/mrsngCuLCGXzsgJPC1FzY7ANQ8AAAAAACJRIP7uXXtl4jLUcVVQU+sX7WFXmx2H6iCMzn7gye0v1Y8JAAAAAAABAStYPg8AAAAAACJRIP7uXXtl4jLUcVVQU+sX7WFXmx2H6iCMzn7gye0v1Y8JIgYCHVQFdXiyvwIlLdnji8FvbUgb+ECuTRNAB67owJnjcaMYAAAAAFYAAIAAAACAAQAAgAAAAAAAAAAAQRSc1yjQ/vHRHev23fKGANLvbOhkNYmGtmRWt8fSszlOJUzRFbnfxd1fq9gIEpaI0vrZww8tlZ94iEL75QoaIbVqQJsPdLYf7fAoXO82VoqwYHu1WevE4g6LxUGBPzfd96q5EEZkoW5qqg+v5dWJUEY467Q6qZLFHwziUaB3KEY8yEpCFcBQkpt0waBJVLeLS2A16XpeB4paDyjsltVHv+6azoA6wO5D2Mh3x0XNGxFCS67GNughkENFodpFeVpZjn76chI8RSAdVAV1eLK/AiUt2eOLwW9tSBv4QK5NE0AHrujAmeNxo60gnNco0P7x0R3r9t3yhgDS72zoZDWJhrZkVrfH0rM5TiWswCEWHVQFdXiyvwIlLdnji8FvbUgb+ECuTRNAB67owJnjcaM5AUzRFbnfxd1fq9gIEpaI0vrZww8tlZ94iEL75QoaIbVqAAAAAFYAAIAAAACAAQAAgAAAAAAAAAAAARcgUJKbdMGgSVS3i0tgNel6XgeKWg8o7JbVR7/ums6AOsAAAAA=", - "unconditionalForfeitTxs": [ - "cHNidP8BAFICAAAAARH6LJRGP/pFIkD/o5bBp8fXAhjl8yjfN7MhJsxdt5lrAQAAAAD/////AZA9DwAAAAAAFgAU+9NJhjFhe8jX1hrXh3NvDyHZ1cYAAAAAAAEBK1g+DwAAAAAAIlEg/u5de2XiMtRxVVBT6xftYVebHYfqIIzOfuDJ7S/VjwlBFJzXKND+8dEd6/bd8oYA0u9s6GQ1iYa2ZFa3x9KzOU4lTNEVud/F3V+r2AgSlojS+tnDDy2Vn3iIQvvlChohtWpARJzBjlEkN/kTpyFEtpvP2Ui7ypevuxb9J/NUAwhYf8Pmnnj1l3WuKCSi4Fcp1O+lQjIiZlNpwY6J73q/V8Fe2kIVwFCSm3TBoElUt4tLYDXpel4HiloPKOyW1Ue/7prOgDrA7kPYyHfHRc0bEUJLrsY26CGQQ0Wh2kV5WlmOfvpyEjxFIB1UBXV4sr8CJS3Z44vBb21IG/hArk0TQAeu6MCZ43GjrSCc1yjQ/vHRHev23fKGANLvbOhkNYmGtmRWt8fSszlOJazAARcgUJKbdMGgSVS3i0tgNel6XgeKWg8o7JbVR7/ums6AOsAAAA==" - ] - } + "redeemTx": "cHNidP8BAIkCAAAAARH6LJRGP/pFIkD/o5bBp8fXAhjl8yjfN7MhJsxdt5lrAQAAAAD/////AtAHAAAAAAAAIlEguuBh3KQUVZp+NHV2sixQ/mrsngCuLCGXzsgJPC1FzY7ANQ8AAAAAACJRIP7uXXtl4jLUcVVQU+sX7WFXmx2H6iCMzn7gye0v1Y8JAAAAAAABAStYPg8AAAAAACJRIP7uXXtl4jLUcVVQU+sX7WFXmx2H6iCMzn7gye0v1Y8JIgYCHVQFdXiyvwIlLdnji8FvbUgb+ECuTRNAB67owJnjcaMYAAAAAFYAAIAAAACAAQAAgAAAAAAAAAAAQRSc1yjQ/vHRHev23fKGANLvbOhkNYmGtmRWt8fSszlOJUzRFbnfxd1fq9gIEpaI0vrZww8tlZ94iEL75QoaIbVqQJsPdLYf7fAoXO82VoqwYHu1WevE4g6LxUGBPzfd96q5EEZkoW5qqg+v5dWJUEY467Q6qZLFHwziUaB3KEY8yEpCFcBQkpt0waBJVLeLS2A16XpeB4paDyjsltVHv+6azoA6wO5D2Mh3x0XNGxFCS67GNughkENFodpFeVpZjn76chI8RSAdVAV1eLK/AiUt2eOLwW9tSBv4QK5NE0AHrujAmeNxo60gnNco0P7x0R3r9t3yhgDS72zoZDWJhrZkVrfH0rM5TiWswCEWHVQFdXiyvwIlLdnji8FvbUgb+ECuTRNAB67owJnjcaM5AUzRFbnfxd1fq9gIEpaI0vrZww8tlZ94iEL75QoaIbVqAAAAAFYAAIAAAACAAQAAgAAAAAAAAAAAARcgUJKbdMGgSVS3i0tgNel6XgeKWg8o7JbVR7/ums6AOsAAAAA=" } ], "spentVtxos": [] @@ -426,8 +386,7 @@ var ( "spentBy": "", "expireAt": "1726503895", "swept": false, - "pending": false, - "pendingData": null + "redeemTx": "" } ], "spentVtxos": [ @@ -445,13 +404,7 @@ var ( "spentBy": "d6684a5b9e6939dccdf07d1f0eaf7fdd7b31de4d123e63e400d23de739800d4e", "expireAt": "1726054928", "swept": false, - "pending": true, - "pendingData": { - "redeemTx": "cHNidP8BAIkCAAAAAWwxXUjG5tidFA0LmUljX//jwW6xWaS6HKyRCw5StsxpAAAAAAD/////AugDAAAAAAAAIlEgt2eR8LtqTP7yUcQtSydeGrRiHnVmHHnZwYjdC23G7MZwSQAAAAAAACJRIKfUzf/o9h+r0v9y4nmyOt9qO8EkDumQPQZGTbEv8fSFAAAAAAABASsgTgAAAAAAACJRIKfUzf/o9h+r0v9y4nmyOt9qO8EkDumQPQZGTbEv8fSFIgYDp9sz9Cto7FhF+C929Y/jJw1zkRWxq6NURd9kX05mShcYAAAAAFYAAIAAAACAAQAAgAAAAAAAAAAAQRSvBIBey3T0IV353FkuGLMmMJFpqHTrliIsJwJsfJkzq7J0B8bQ0j9842h5lUfOWcbj2TeoFx6OCpgoHIqWIBhHQAFqkBLiRmP3AZ8MS77s1QIWZswMV3L72D9gN0f0MbD6XHkmzZeC1clF3uzxr+13wsF0vcFe29Zl3e2gAhMNGYVCFcFQkpt0waBJVLeLS2A16XpeB4paDyjsltVHv+6azoA6wKRtST8P7teUpSF4DAEbfJj5OIXITx5QGbZns/AtxqGyRSCn2zP0K2jsWEX4L3b1j+MnDXORFbGro1RF32RfTmZKF60grwSAXst09CFd+dxZLhizJjCRaah065YiLCcCbHyZM6uswCEWp9sz9Cto7FhF+C929Y/jJw1zkRWxq6NURd9kX05mShc5AbJ0B8bQ0j9842h5lUfOWcbj2TeoFx6OCpgoHIqWIBhHAAAAAFYAAIAAAACAAQAAgAAAAAAAAAAAARcgUJKbdMGgSVS3i0tgNel6XgeKWg8o7JbVR7/ums6AOsAAAAA=", - "unconditionalForfeitTxs": [ - "cHNidP8BAFICAAAAAWwxXUjG5tidFA0LmUljX//jwW6xWaS6HKyRCw5StsxpAAAAAAD/////AVhNAAAAAAAAFgAUSU38/3Mzx5BdILG4oUO+JoHcoT8AAAAAAAEBKyBOAAAAAAAAIlEgp9TN/+j2H6vS/3LiebI632o7wSQO6ZA9BkZNsS/x9IVBFK8EgF7LdPQhXfncWS4YsyYwkWmodOuWIiwnAmx8mTOrsnQHxtDSP3zjaHmVR85ZxuPZN6gXHo4KmCgcipYgGEdAjH8Mg1Z3GdjGzp78Mg2xq1fop9KDfeji+xoyMgYS7q0Nl0AGOAaNzkDRW4cNcefll5jZC2i3nfygKdXsUsR+LEIVwVCSm3TBoElUt4tLYDXpel4HiloPKOyW1Ue/7prOgDrApG1JPw/u15SlIXgMARt8mPk4hchPHlAZtmez8C3GobJFIKfbM/QraOxYRfgvdvWP4ycNc5EVsaujVEXfZF9OZkoXrSCvBIBey3T0IV353FkuGLMmMJFpqHTrliIsJwJsfJkzq6zAARcgUJKbdMGgSVS3i0tgNel6XgeKWg8o7JbVR7/ums6AOsAAAA==" - ] - } + "redeemTx": "cHNidP8BAIkCAAAAAWwxXUjG5tidFA0LmUljX//jwW6xWaS6HKyRCw5StsxpAAAAAAD/////AugDAAAAAAAAIlEgt2eR8LtqTP7yUcQtSydeGrRiHnVmHHnZwYjdC23G7MZwSQAAAAAAACJRIKfUzf/o9h+r0v9y4nmyOt9qO8EkDumQPQZGTbEv8fSFAAAAAAABASsgTgAAAAAAACJRIKfUzf/o9h+r0v9y4nmyOt9qO8EkDumQPQZGTbEv8fSFIgYDp9sz9Cto7FhF+C929Y/jJw1zkRWxq6NURd9kX05mShcYAAAAAFYAAIAAAACAAQAAgAAAAAAAAAAAQRSvBIBey3T0IV353FkuGLMmMJFpqHTrliIsJwJsfJkzq7J0B8bQ0j9842h5lUfOWcbj2TeoFx6OCpgoHIqWIBhHQAFqkBLiRmP3AZ8MS77s1QIWZswMV3L72D9gN0f0MbD6XHkmzZeC1clF3uzxr+13wsF0vcFe29Zl3e2gAhMNGYVCFcFQkpt0waBJVLeLS2A16XpeB4paDyjsltVHv+6azoA6wKRtST8P7teUpSF4DAEbfJj5OIXITx5QGbZns/AtxqGyRSCn2zP0K2jsWEX4L3b1j+MnDXORFbGro1RF32RfTmZKF60grwSAXst09CFd+dxZLhizJjCRaah065YiLCcCbHyZM6uswCEWp9sz9Cto7FhF+C929Y/jJw1zkRWxq6NURd9kX05mShc5AbJ0B8bQ0j9842h5lUfOWcbj2TeoFx6OCpgoHIqWIBhHAAAAAFYAAIAAAACAAQAAgAAAAAAAAAAAARcgUJKbdMGgSVS3i0tgNel6XgeKWg8o7JbVR7/ums6AOsAAAAA=" }, { "outpoint": { @@ -467,13 +420,7 @@ var ( "spentBy": "d6684a5b9e6939dccdf07d1f0eaf7fdd7b31de4d123e63e400d23de739800d4e", "expireAt": "1726486389", "swept": false, - "pending": true, - "pendingData": { - "redeemTx": "cHNidP8BAIkCAAAAARH6LJRGP/pFIkD/o5bBp8fXAhjl8yjfN7MhJsxdt5lrAQAAAAD/////AtAHAAAAAAAAIlEguuBh3KQUVZp+NHV2sixQ/mrsngCuLCGXzsgJPC1FzY7ANQ8AAAAAACJRIP7uXXtl4jLUcVVQU+sX7WFXmx2H6iCMzn7gye0v1Y8JAAAAAAABAStYPg8AAAAAACJRIP7uXXtl4jLUcVVQU+sX7WFXmx2H6iCMzn7gye0v1Y8JIgYCHVQFdXiyvwIlLdnji8FvbUgb+ECuTRNAB67owJnjcaMYAAAAAFYAAIAAAACAAQAAgAAAAAAAAAAAQRSc1yjQ/vHRHev23fKGANLvbOhkNYmGtmRWt8fSszlOJUzRFbnfxd1fq9gIEpaI0vrZww8tlZ94iEL75QoaIbVqQJsPdLYf7fAoXO82VoqwYHu1WevE4g6LxUGBPzfd96q5EEZkoW5qqg+v5dWJUEY467Q6qZLFHwziUaB3KEY8yEpCFcBQkpt0waBJVLeLS2A16XpeB4paDyjsltVHv+6azoA6wO5D2Mh3x0XNGxFCS67GNughkENFodpFeVpZjn76chI8RSAdVAV1eLK/AiUt2eOLwW9tSBv4QK5NE0AHrujAmeNxo60gnNco0P7x0R3r9t3yhgDS72zoZDWJhrZkVrfH0rM5TiWswCEWHVQFdXiyvwIlLdnji8FvbUgb+ECuTRNAB67owJnjcaM5AUzRFbnfxd1fq9gIEpaI0vrZww8tlZ94iEL75QoaIbVqAAAAAFYAAIAAAACAAQAAgAAAAAAAAAAAARcgUJKbdMGgSVS3i0tgNel6XgeKWg8o7JbVR7/ums6AOsAAAAA=", - "unconditionalForfeitTxs": [ - "cHNidP8BAFICAAAAARH6LJRGP/pFIkD/o5bBp8fXAhjl8yjfN7MhJsxdt5lrAQAAAAD/////AZA9DwAAAAAAFgAU+9NJhjFhe8jX1hrXh3NvDyHZ1cYAAAAAAAEBK1g+DwAAAAAAIlEg/u5de2XiMtRxVVBT6xftYVebHYfqIIzOfuDJ7S/VjwlBFJzXKND+8dEd6/bd8oYA0u9s6GQ1iYa2ZFa3x9KzOU4lTNEVud/F3V+r2AgSlojS+tnDDy2Vn3iIQvvlChohtWpARJzBjlEkN/kTpyFEtpvP2Ui7ypevuxb9J/NUAwhYf8Pmnnj1l3WuKCSi4Fcp1O+lQjIiZlNpwY6J73q/V8Fe2kIVwFCSm3TBoElUt4tLYDXpel4HiloPKOyW1Ue/7prOgDrA7kPYyHfHRc0bEUJLrsY26CGQQ0Wh2kV5WlmOfvpyEjxFIB1UBXV4sr8CJS3Z44vBb21IG/hArk0TQAeu6MCZ43GjrSCc1yjQ/vHRHev23fKGANLvbOhkNYmGtmRWt8fSszlOJazAARcgUJKbdMGgSVS3i0tgNel6XgeKWg8o7JbVR7/ums6AOsAAAA==" - ] - } + "redeemTx": "cHNidP8BAIkCAAAAARH6LJRGP/pFIkD/o5bBp8fXAhjl8yjfN7MhJsxdt5lrAQAAAAD/////AtAHAAAAAAAAIlEguuBh3KQUVZp+NHV2sixQ/mrsngCuLCGXzsgJPC1FzY7ANQ8AAAAAACJRIP7uXXtl4jLUcVVQU+sX7WFXmx2H6iCMzn7gye0v1Y8JAAAAAAABAStYPg8AAAAAACJRIP7uXXtl4jLUcVVQU+sX7WFXmx2H6iCMzn7gye0v1Y8JIgYCHVQFdXiyvwIlLdnji8FvbUgb+ECuTRNAB67owJnjcaMYAAAAAFYAAIAAAACAAQAAgAAAAAAAAAAAQRSc1yjQ/vHRHev23fKGANLvbOhkNYmGtmRWt8fSszlOJUzRFbnfxd1fq9gIEpaI0vrZww8tlZ94iEL75QoaIbVqQJsPdLYf7fAoXO82VoqwYHu1WevE4g6LxUGBPzfd96q5EEZkoW5qqg+v5dWJUEY467Q6qZLFHwziUaB3KEY8yEpCFcBQkpt0waBJVLeLS2A16XpeB4paDyjsltVHv+6azoA6wO5D2Mh3x0XNGxFCS67GNughkENFodpFeVpZjn76chI8RSAdVAV1eLK/AiUt2eOLwW9tSBv4QK5NE0AHrujAmeNxo60gnNco0P7x0R3r9t3yhgDS72zoZDWJhrZkVrfH0rM5TiWswCEWHVQFdXiyvwIlLdnji8FvbUgb+ECuTRNAB67owJnjcaM5AUzRFbnfxd1fq9gIEpaI0vrZww8tlZ94iEL75QoaIbVqAAAAAFYAAIAAAACAAQAAgAAAAAAAAAAAARcgUJKbdMGgSVS3i0tgNel6XgeKWg8o7JbVR7/ums6AOsAAAAA=" } ] }` @@ -494,13 +441,7 @@ var ( "spentBy": "", "expireAt": "1726503895", "swept": false, - "pending": false, - "pendingData": { - "redeemTx": "cHNidP8BAIkCAAAAAdOK9YzYw1ceJznqJxtRXGe0KeHj6CLcLtqLVwcbMCivAAAAAAD/////ArgLAAAAAAAAIlEgC39Vxhw3dIa4heHgFS6X4XwDl1mBggsKLVTBwF1h3qEgegEAAAAAACJRIMkktfIFxFNTtAmy3K0p+7JqVn2kcA0P6y2vJ1QX2zysAAAAAAABASughgEAAAAAACJRIMkktfIFxFNTtAmy3K0p+7JqVn2kcA0P6y2vJ1QX2zysIgYDjGeMfnNwCrU45iB3iRqiFdWTADaiJ968+w3ruFuq1F0YAAAAAFYAAIAAAACAAQAAgAAAAAAAAAAAQRTYEOuHJ0hyLBGzY8nSHpD2F1nby5/XQ5Sh2Je+cQ5Wsx0ZucLmB/LLspxMRN9JcJn3Q2KJRMhhg7415cCg1d0gQNSvgaBk/1WLYqQxCKxCfv8ViVJ7vjBxvNO5tc2FEDy27V9cIrfL1jPJoVrhgPZT0GwY7dkVZS7saIKI03CbipBCFcBQkpt0waBJVLeLS2A16XpeB4paDyjsltVHv+6azoA6wPKiQ0JM6aw2kcUByijEbOydM3gTIVCGN/69q+dmyxcqRSCMZ4x+c3AKtTjmIHeJGqIV1ZMANqIn3rz7Deu4W6rUXa0g2BDrhydIciwRs2PJ0h6Q9hdZ28uf10OUodiXvnEOVrOswCEWjGeMfnNwCrU45iB3iRqiFdWTADaiJ968+w3ruFuq1F05AR0ZucLmB/LLspxMRN9JcJn3Q2KJRMhhg7415cCg1d0gAAAAAFYAAIAAAACAAQAAgAAAAAAAAAAAARcgUJKbdMGgSVS3i0tgNel6XgeKWg8o7JbVR7/ums6AOsAAAAA=", - "unconditionalForfeitTxs": [ - "cHNidP8BAFICAAAAAdOK9YzYw1ceJznqJxtRXGe0KeHj6CLcLtqLVwcbMCivAAAAAAD/////AdiFAQAAAAAAFgAUlsBYsQa9BEiB8ZumuN4J50lbQIoAAAAAAAEBK6CGAQAAAAAAIlEgySS18gXEU1O0CbLcrSn7smpWfaRwDQ/rLa8nVBfbPKxBFNgQ64cnSHIsEbNjydIekPYXWdvLn9dDlKHYl75xDlazHRm5wuYH8suynExE30lwmfdDYolEyGGDvjXlwKDV3SBAZadgbU8gCDvq3XN0EeLIwGKGSAYHZRkGbAnr9ZjCHGKAQlfFNYS0af1Lz4j7Th2osVY8JJv7O736sC5NNQome0IVwFCSm3TBoElUt4tLYDXpel4HiloPKOyW1Ue/7prOgDrA8qJDQkzprDaRxQHKKMRs7J0zeBMhUIY3/r2r52bLFypFIIxnjH5zcAq1OOYgd4kaohXVkwA2oifevPsN67hbqtRdrSDYEOuHJ0hyLBGzY8nSHpD2F1nby5/XQ5Sh2Je+cQ5Ws6zAARcgUJKbdMGgSVS3i0tgNel6XgeKWg8o7JbVR7/ums6AOsAAAA==" - ] - } + "redeemTx": "cHNidP8BAIkCAAAAAdOK9YzYw1ceJznqJxtRXGe0KeHj6CLcLtqLVwcbMCivAAAAAAD/////ArgLAAAAAAAAIlEgC39Vxhw3dIa4heHgFS6X4XwDl1mBggsKLVTBwF1h3qEgegEAAAAAACJRIMkktfIFxFNTtAmy3K0p+7JqVn2kcA0P6y2vJ1QX2zysAAAAAAABASughgEAAAAAACJRIMkktfIFxFNTtAmy3K0p+7JqVn2kcA0P6y2vJ1QX2zysIgYDjGeMfnNwCrU45iB3iRqiFdWTADaiJ968+w3ruFuq1F0YAAAAAFYAAIAAAACAAQAAgAAAAAAAAAAAQRTYEOuHJ0hyLBGzY8nSHpD2F1nby5/XQ5Sh2Je+cQ5Wsx0ZucLmB/LLspxMRN9JcJn3Q2KJRMhhg7415cCg1d0gQNSvgaBk/1WLYqQxCKxCfv8ViVJ7vjBxvNO5tc2FEDy27V9cIrfL1jPJoVrhgPZT0GwY7dkVZS7saIKI03CbipBCFcBQkpt0waBJVLeLS2A16XpeB4paDyjsltVHv+6azoA6wPKiQ0JM6aw2kcUByijEbOydM3gTIVCGN/69q+dmyxcqRSCMZ4x+c3AKtTjmIHeJGqIV1ZMANqIn3rz7Deu4W6rUXa0g2BDrhydIciwRs2PJ0h6Q9hdZ28uf10OUodiXvnEOVrOswCEWjGeMfnNwCrU45iB3iRqiFdWTADaiJ968+w3ruFuq1F05AR0ZucLmB/LLspxMRN9JcJn3Q2KJRMhhg7415cCg1d0gAAAAAFYAAIAAAACAAQAAgAAAAAAAAAAAARcgUJKbdMGgSVS3i0tgNel6XgeKWg8o7JbVR7/ums6AOsAAAAA=" } ], "spentVtxos": [ @@ -518,13 +459,7 @@ var ( "spentBy": "d6684a5b9e6939dccdf07d1f0eaf7fdd7b31de4d123e63e400d23de739800d4e", "expireAt": "1726054928", "swept": false, - "pending": true, - "pendingData": { - "redeemTx": "cHNidP8BAIkCAAAAAWwxXUjG5tidFA0LmUljX//jwW6xWaS6HKyRCw5StsxpAAAAAAD/////AugDAAAAAAAAIlEgt2eR8LtqTP7yUcQtSydeGrRiHnVmHHnZwYjdC23G7MZwSQAAAAAAACJRIKfUzf/o9h+r0v9y4nmyOt9qO8EkDumQPQZGTbEv8fSFAAAAAAABASsgTgAAAAAAACJRIKfUzf/o9h+r0v9y4nmyOt9qO8EkDumQPQZGTbEv8fSFIgYDp9sz9Cto7FhF+C929Y/jJw1zkRWxq6NURd9kX05mShcYAAAAAFYAAIAAAACAAQAAgAAAAAAAAAAAQRSvBIBey3T0IV353FkuGLMmMJFpqHTrliIsJwJsfJkzq7J0B8bQ0j9842h5lUfOWcbj2TeoFx6OCpgoHIqWIBhHQAFqkBLiRmP3AZ8MS77s1QIWZswMV3L72D9gN0f0MbD6XHkmzZeC1clF3uzxr+13wsF0vcFe29Zl3e2gAhMNGYVCFcFQkpt0waBJVLeLS2A16XpeB4paDyjsltVHv+6azoA6wKRtST8P7teUpSF4DAEbfJj5OIXITx5QGbZns/AtxqGyRSCn2zP0K2jsWEX4L3b1j+MnDXORFbGro1RF32RfTmZKF60grwSAXst09CFd+dxZLhizJjCRaah065YiLCcCbHyZM6uswCEWp9sz9Cto7FhF+C929Y/jJw1zkRWxq6NURd9kX05mShc5AbJ0B8bQ0j9842h5lUfOWcbj2TeoFx6OCpgoHIqWIBhHAAAAAFYAAIAAAACAAQAAgAAAAAAAAAAAARcgUJKbdMGgSVS3i0tgNel6XgeKWg8o7JbVR7/ums6AOsAAAAA=", - "unconditionalForfeitTxs": [ - "cHNidP8BAFICAAAAAWwxXUjG5tidFA0LmUljX//jwW6xWaS6HKyRCw5StsxpAAAAAAD/////AVhNAAAAAAAAFgAUSU38/3Mzx5BdILG4oUO+JoHcoT8AAAAAAAEBKyBOAAAAAAAAIlEgp9TN/+j2H6vS/3LiebI632o7wSQO6ZA9BkZNsS/x9IVBFK8EgF7LdPQhXfncWS4YsyYwkWmodOuWIiwnAmx8mTOrsnQHxtDSP3zjaHmVR85ZxuPZN6gXHo4KmCgcipYgGEdAjH8Mg1Z3GdjGzp78Mg2xq1fop9KDfeji+xoyMgYS7q0Nl0AGOAaNzkDRW4cNcefll5jZC2i3nfygKdXsUsR+LEIVwVCSm3TBoElUt4tLYDXpel4HiloPKOyW1Ue/7prOgDrApG1JPw/u15SlIXgMARt8mPk4hchPHlAZtmez8C3GobJFIKfbM/QraOxYRfgvdvWP4ycNc5EVsaujVEXfZF9OZkoXrSCvBIBey3T0IV353FkuGLMmMJFpqHTrliIsJwJsfJkzq6zAARcgUJKbdMGgSVS3i0tgNel6XgeKWg8o7JbVR7/ums6AOsAAAA==" - ] - } + "redeemTx": "cHNidP8BAIkCAAAAAWwxXUjG5tidFA0LmUljX//jwW6xWaS6HKyRCw5StsxpAAAAAAD/////AugDAAAAAAAAIlEgt2eR8LtqTP7yUcQtSydeGrRiHnVmHHnZwYjdC23G7MZwSQAAAAAAACJRIKfUzf/o9h+r0v9y4nmyOt9qO8EkDumQPQZGTbEv8fSFAAAAAAABASsgTgAAAAAAACJRIKfUzf/o9h+r0v9y4nmyOt9qO8EkDumQPQZGTbEv8fSFIgYDp9sz9Cto7FhF+C929Y/jJw1zkRWxq6NURd9kX05mShcYAAAAAFYAAIAAAACAAQAAgAAAAAAAAAAAQRSvBIBey3T0IV353FkuGLMmMJFpqHTrliIsJwJsfJkzq7J0B8bQ0j9842h5lUfOWcbj2TeoFx6OCpgoHIqWIBhHQAFqkBLiRmP3AZ8MS77s1QIWZswMV3L72D9gN0f0MbD6XHkmzZeC1clF3uzxr+13wsF0vcFe29Zl3e2gAhMNGYVCFcFQkpt0waBJVLeLS2A16XpeB4paDyjsltVHv+6azoA6wKRtST8P7teUpSF4DAEbfJj5OIXITx5QGbZns/AtxqGyRSCn2zP0K2jsWEX4L3b1j+MnDXORFbGro1RF32RfTmZKF60grwSAXst09CFd+dxZLhizJjCRaah065YiLCcCbHyZM6uswCEWp9sz9Cto7FhF+C929Y/jJw1zkRWxq6NURd9kX05mShc5AbJ0B8bQ0j9842h5lUfOWcbj2TeoFx6OCpgoHIqWIBhHAAAAAFYAAIAAAACAAQAAgAAAAAAAAAAAARcgUJKbdMGgSVS3i0tgNel6XgeKWg8o7JbVR7/ums6AOsAAAAA=" }, { "outpoint": { @@ -541,12 +476,7 @@ var ( "expireAt": "1726486389", "swept": false, "pending": true, - "pendingData": { - "redeemTx": "cHNidP8BAIkCAAAAARH6LJRGP/pFIkD/o5bBp8fXAhjl8yjfN7MhJsxdt5lrAQAAAAD/////AtAHAAAAAAAAIlEguuBh3KQUVZp+NHV2sixQ/mrsngCuLCGXzsgJPC1FzY7ANQ8AAAAAACJRIP7uXXtl4jLUcVVQU+sX7WFXmx2H6iCMzn7gye0v1Y8JAAAAAAABAStYPg8AAAAAACJRIP7uXXtl4jLUcVVQU+sX7WFXmx2H6iCMzn7gye0v1Y8JIgYCHVQFdXiyvwIlLdnji8FvbUgb+ECuTRNAB67owJnjcaMYAAAAAFYAAIAAAACAAQAAgAAAAAAAAAAAQRSc1yjQ/vHRHev23fKGANLvbOhkNYmGtmRWt8fSszlOJUzRFbnfxd1fq9gIEpaI0vrZww8tlZ94iEL75QoaIbVqQJsPdLYf7fAoXO82VoqwYHu1WevE4g6LxUGBPzfd96q5EEZkoW5qqg+v5dWJUEY467Q6qZLFHwziUaB3KEY8yEpCFcBQkpt0waBJVLeLS2A16XpeB4paDyjsltVHv+6azoA6wO5D2Mh3x0XNGxFCS67GNughkENFodpFeVpZjn76chI8RSAdVAV1eLK/AiUt2eOLwW9tSBv4QK5NE0AHrujAmeNxo60gnNco0P7x0R3r9t3yhgDS72zoZDWJhrZkVrfH0rM5TiWswCEWHVQFdXiyvwIlLdnji8FvbUgb+ECuTRNAB67owJnjcaM5AUzRFbnfxd1fq9gIEpaI0vrZww8tlZ94iEL75QoaIbVqAAAAAFYAAIAAAACAAQAAgAAAAAAAAAAAARcgUJKbdMGgSVS3i0tgNel6XgeKWg8o7JbVR7/ums6AOsAAAAA=", - "unconditionalForfeitTxs": [ - "cHNidP8BAFICAAAAARH6LJRGP/pFIkD/o5bBp8fXAhjl8yjfN7MhJsxdt5lrAQAAAAD/////AZA9DwAAAAAAFgAU+9NJhjFhe8jX1hrXh3NvDyHZ1cYAAAAAAAEBK1g+DwAAAAAAIlEg/u5de2XiMtRxVVBT6xftYVebHYfqIIzOfuDJ7S/VjwlBFJzXKND+8dEd6/bd8oYA0u9s6GQ1iYa2ZFa3x9KzOU4lTNEVud/F3V+r2AgSlojS+tnDDy2Vn3iIQvvlChohtWpARJzBjlEkN/kTpyFEtpvP2Ui7ypevuxb9J/NUAwhYf8Pmnnj1l3WuKCSi4Fcp1O+lQjIiZlNpwY6J73q/V8Fe2kIVwFCSm3TBoElUt4tLYDXpel4HiloPKOyW1Ue/7prOgDrA7kPYyHfHRc0bEUJLrsY26CGQQ0Wh2kV5WlmOfvpyEjxFIB1UBXV4sr8CJS3Z44vBb21IG/hArk0TQAeu6MCZ43GjrSCc1yjQ/vHRHev23fKGANLvbOhkNYmGtmRWt8fSszlOJazAARcgUJKbdMGgSVS3i0tgNel6XgeKWg8o7JbVR7/ums6AOsAAAA==" - ] - } + "redeemTx": "cHNidP8BAIkCAAAAARH6LJRGP/pFIkD/o5bBp8fXAhjl8yjfN7MhJsxdt5lrAQAAAAD/////AtAHAAAAAAAAIlEguuBh3KQUVZp+NHV2sixQ/mrsngCuLCGXzsgJPC1FzY7ANQ8AAAAAACJRIP7uXXtl4jLUcVVQU+sX7WFXmx2H6iCMzn7gye0v1Y8JAAAAAAABAStYPg8AAAAAACJRIP7uXXtl4jLUcVVQU+sX7WFXmx2H6iCMzn7gye0v1Y8JIgYCHVQFdXiyvwIlLdnji8FvbUgb+ECuTRNAB67owJnjcaMYAAAAAFYAAIAAAACAAQAAgAAAAAAAAAAAQRSc1yjQ/vHRHev23fKGANLvbOhkNYmGtmRWt8fSszlOJUzRFbnfxd1fq9gIEpaI0vrZww8tlZ94iEL75QoaIbVqQJsPdLYf7fAoXO82VoqwYHu1WevE4g6LxUGBPzfd96q5EEZkoW5qqg+v5dWJUEY467Q6qZLFHwziUaB3KEY8yEpCFcBQkpt0waBJVLeLS2A16XpeB4paDyjsltVHv+6azoA6wO5D2Mh3x0XNGxFCS67GNughkENFodpFeVpZjn76chI8RSAdVAV1eLK/AiUt2eOLwW9tSBv4QK5NE0AHrujAmeNxo60gnNco0P7x0R3r9t3yhgDS72zoZDWJhrZkVrfH0rM5TiWswCEWHVQFdXiyvwIlLdnji8FvbUgb+ECuTRNAB67owJnjcaM5AUzRFbnfxd1fq9gIEpaI0vrZww8tlZ94iEL75QoaIbVqAAAAAFYAAIAAAACAAQAAgAAAAAAAAAAAARcgUJKbdMGgSVS3i0tgNel6XgeKWg8o7JbVR7/ums6AOsAAAAA=" }, { "outpoint": { @@ -562,8 +492,7 @@ var ( "spentBy": "23c3a885f0ea05f7bdf83f3bf7f8ac9dc3f791ad292f4e63a6f53fa5e4935ab0", "expireAt": "1726503895", "swept": false, - "pending": false, - "pendingData": null + "redeemTx": "" } ] }` diff --git a/pkg/client-sdk/covenant_client.go b/pkg/client-sdk/covenant_client.go index 5a0fd3c..2af5c0d 100644 --- a/pkg/client-sdk/covenant_client.go +++ b/pkg/client-sdk/covenant_client.go @@ -14,7 +14,6 @@ import ( "github.com/ark-network/ark/common" "github.com/ark-network/ark/common/tree" "github.com/ark-network/ark/pkg/client-sdk/client" - "github.com/ark-network/ark/pkg/client-sdk/explorer" "github.com/ark-network/ark/pkg/client-sdk/internal/utils" "github.com/ark-network/ark/pkg/client-sdk/redemption" "github.com/ark-network/ark/pkg/client-sdk/types" @@ -251,26 +250,6 @@ func (a *covenantArkClient) listenForBoardingUtxos( //are multiple boarding inputs + spent vtxo with change in spendable + received in the same round } -func (a *covenantArkClient) ListVtxos( - ctx context.Context, -) (spendableVtxos, spentVtxos []client.Vtxo, err error) { - offchainAddrs, _, _, err := a.wallet.GetAddresses(ctx) - if err != nil { - return - } - - for _, addr := range offchainAddrs { - spendable, spent, err := a.client.ListVtxos(ctx, addr.Address) - if err != nil { - return nil, nil, err - } - spendableVtxos = append(spendableVtxos, spendable...) - spentVtxos = append(spentVtxos, spent...) - } - - return -} - func (a *covenantArkClient) Balance( ctx context.Context, computeVtxoExpiration bool, ) (*Balance, error) { @@ -285,14 +264,13 @@ func (a *covenantArkClient) Balance( chRes := make(chan balanceRes, nbWorkers*len(offchainAddrs)) for i := range offchainAddrs { - offchainAddr := offchainAddrs[i] boardingAddr := boardingAddrs[i] redeemAddr := redeemAddrs[i] - go func(addr string) { + go func() { defer wg.Done() balance, amountByExpiration, err := a.getOffchainBalance( - ctx, addr, computeVtxoExpiration, + ctx, computeVtxoExpiration, ) if err != nil { chRes <- balanceRes{err: err} @@ -303,7 +281,7 @@ func (a *covenantArkClient) Balance( offchainBalance: balance, offchainBalanceByExpiration: amountByExpiration, } - }(offchainAddr.Address) + }() getDelayedBalance := func(addr string) { defer wg.Done() @@ -448,20 +426,11 @@ func (a *covenantArkClient) UnilateralRedeem(ctx context.Context) error { return fmt.Errorf("wallet is locked") } - offchainAddrs, _, _, err := a.wallet.GetAddresses(ctx) + vtxos, err := a.getVtxos(ctx, false, nil) if err != nil { return err } - vtxos := make([]client.Vtxo, 0) - for _, offchainAddr := range offchainAddrs { - fetchedVtxos, _, err := a.client.ListVtxos(ctx, offchainAddr.Address) - if err != nil { - return err - } - vtxos = append(vtxos, fetchedVtxos...) - } - totalVtxosAmount := uint64(0) for _, vtxo := range vtxos { totalVtxosAmount += vtxo.Amount @@ -519,22 +488,13 @@ func (a *covenantArkClient) CollaborativeRedeem( return "", fmt.Errorf("wallet is locked") } + // validate liquid address if _, err := address.ToOutputScript(addr); err != nil { return "", fmt.Errorf("invalid onchain address") } - addrNet, err := address.NetworkForAddress(addr) - if err != nil { - return "", fmt.Errorf("invalid onchain address: unknown network") - } - net := utils.ToElementsNetwork(a.Network) - if net.Name != addrNet.Name { - return "", fmt.Errorf("invalid onchain address: must be for %s network", net.Name) - } - - if isConf, _ := address.IsConfidential(addr); isConf { - info, _ := address.FromConfidential(addr) - addr = info.Address + if isConf, err := address.IsConfidential(addr); err != nil || isConf { + return "", fmt.Errorf("confidential onchain address not supported") } offchainAddrs, _, _, err := a.wallet.GetAddresses(ctx) @@ -550,22 +510,34 @@ func (a *covenantArkClient) CollaborativeRedeem( } vtxos := make([]client.DescriptorVtxo, 0) - for _, offchainAddr := range offchainAddrs { - spendableVtxos, err := a.getVtxos(ctx, offchainAddr.Address, withExpiryCoinselect) - if err != nil { - return "", err - } + spendableVtxos, err := a.getVtxos(ctx, false, nil) + if err != nil { + return "", err + } - for _, vtxo := range spendableVtxos { - vtxos = append(vtxos, client.DescriptorVtxo{ - Vtxo: vtxo, - Descriptor: offchainAddr.Descriptor, - }) + for _, offchainAddr := range offchainAddrs { + for _, v := range spendableVtxos { + vtxoAddr, err := v.Address(a.AspPubkey, a.Network) + if err != nil { + return "", err + } + + if vtxoAddr == offchainAddr.Address { + vtxos = append(vtxos, client.DescriptorVtxo{ + Vtxo: v, + Descriptor: offchainAddr.Descriptor, + }) + } } } - selectedCoins, changeAmount, err := utils.CoinSelect( - vtxos, amount, a.Dust, withExpiryCoinselect, + boardingUtxos, err := a.getClaimableBoardingUtxos(ctx, nil) + if err != nil { + return "", err + } + + selectedBoardingUtxos, selectedCoins, changeAmount, err := utils.CoinSelect( + boardingUtxos, vtxos, amount, a.Dust, withExpiryCoinselect, ) if err != nil { return "", err @@ -583,7 +555,7 @@ func (a *covenantArkClient) CollaborativeRedeem( }) } - inputs := make([]client.Input, 0, len(selectedCoins)) + inputs := make([]client.Input, 0, len(selectedCoins)+len(selectedBoardingUtxos)) for _, coin := range selectedCoins { inputs = append(inputs, client.Input{ @@ -594,8 +566,17 @@ func (a *covenantArkClient) CollaborativeRedeem( Descriptor: coin.Descriptor, }) } + for _, coin := range selectedBoardingUtxos { + inputs = append(inputs, client.Input{ + Outpoint: client.Outpoint{ + Txid: coin.Txid, + VOut: coin.VOut, + }, + Descriptor: coin.Descriptor, + }) + } - paymentID, err := a.client.RegisterInputsForNextRound(ctx, inputs, "") // ephemeralPublicKey is not required for covenant + paymentID, err := a.client.RegisterInputsForNextRound(ctx, inputs, "") if err != nil { return "", err } @@ -604,9 +585,7 @@ func (a *covenantArkClient) CollaborativeRedeem( return "", err } - poolTxID, err := a.handleRoundStream( - ctx, paymentID, selectedCoins, nil, "", receivers, - ) + poolTxID, err := a.handleRoundStream(ctx, paymentID, selectedCoins, selectedBoardingUtxos, receivers) if err != nil { return "", err } @@ -621,36 +600,10 @@ func (a *covenantArkClient) SendAsync( return "", fmt.Errorf("not implemented") } -func (a *covenantArkClient) Claim(ctx context.Context) (string, error) { - myselfOffchain, boardingAddr, err := a.wallet.NewAddress(ctx, false) - if err != nil { - return "", err - } - - boardingUtxos, err := a.getClaimableBoardingUtxos(ctx) - if err != nil { - return "", err - } - - var pendingBalance uint64 - for _, vtxo := range boardingUtxos { - pendingBalance += vtxo.Amount - } - if pendingBalance == 0 { - return "", fmt.Errorf("no funds to claim") - } - - receiver := client.Output{ - Address: myselfOffchain.Address, - Amount: pendingBalance, - } - - return a.selfTransferAllPendingPayments( - ctx, - boardingUtxos, - receiver, - boardingAddr.Descriptor, - ) +func (a *covenantArkClient) Settle( + ctx context.Context, +) (string, error) { + return a.sendOffchain(ctx, false, nil) } func (a *covenantArkClient) GetTransactionHistory( @@ -675,13 +628,13 @@ func (a *covenantArkClient) GetTransactionHistory( return vtxosToTxsCovenant(config.RoundLifetime, spendableVtxos, spentVtxos, boardingTxs) } -func (a *covenantArkClient) getAllBoardingUtxos(ctx context.Context) ([]explorer.Utxo, error) { +func (a *covenantArkClient) getAllBoardingUtxos(ctx context.Context) ([]types.Utxo, error) { _, boardingAddrs, _, err := a.wallet.GetAddresses(ctx) if err != nil { return nil, err } - utxos := []explorer.Utxo{} + utxos := make([]types.Utxo, 0) for _, addr := range boardingAddrs { txs, err := a.explorer.GetTxs(addr.Address) if err != nil { @@ -694,11 +647,12 @@ func (a *covenantArkClient) getAllBoardingUtxos(ctx context.Context) ([]explorer if tx.Status.Confirmed { createdAt = time.Unix(tx.Status.Blocktime, 0) } - utxos = append(utxos, explorer.Utxo{ - Txid: tx.Txid, - Vout: uint32(i), - Amount: vout.Amount, - CreatedAt: createdAt, + utxos = append(utxos, types.Utxo{ + Txid: tx.Txid, + VOut: uint32(i), + Amount: vout.Amount, + CreatedAt: createdAt, + Descriptor: addr.Descriptor, }) } } @@ -708,13 +662,13 @@ func (a *covenantArkClient) getAllBoardingUtxos(ctx context.Context) ([]explorer return utxos, nil } -func (a *covenantArkClient) getClaimableBoardingUtxos(ctx context.Context) ([]explorer.Utxo, error) { +func (a *covenantArkClient) getClaimableBoardingUtxos(ctx context.Context, opts *CoinSelectOptions) ([]types.Utxo, error) { _, boardingAddrs, _, err := a.wallet.GetAddresses(ctx) if err != nil { return nil, err } - claimable := make([]explorer.Utxo, 0) + claimable := make([]types.Utxo, 0) for _, addr := range boardingAddrs { boardingScript, err := tree.ParseVtxoScript(addr.Descriptor) @@ -738,8 +692,25 @@ func (a *covenantArkClient) getClaimableBoardingUtxos(ctx context.Context) ([]ex now := time.Now() for _, utxo := range boardingUtxos { - u := utxo.ToUtxo(boardingTimeout) + if opts != nil && len(opts.OutpointsFilter) > 0 { + utxoOutpoint := client.Outpoint{ + Txid: utxo.Txid, + VOut: utxo.Vout, + } + found := false + for _, outpoint := range opts.OutpointsFilter { + if outpoint.Equals(utxoOutpoint) { + found = true + break + } + } + if !found { + continue + } + } + + u := utxo.ToUtxo(boardingTimeout, addr.Descriptor) if u.SpendableAt.Before(now) { continue } @@ -906,84 +877,123 @@ func (a *covenantArkClient) sendOnchain( } func (a *covenantArkClient) sendOffchain( - ctx context.Context, withExpiryCoinselect bool, receivers []Receiver, + ctx context.Context, + withExpiryCoinselect bool, receivers []Receiver, ) (string, error) { if a.wallet.IsLocked() { return "", fmt.Errorf("wallet is locked") } + expectedAspPubKey := schnorr.SerializePubKey(a.AspPubkey) + outputs := make([]client.Output, 0) + sumOfReceivers := uint64(0) + + // validate receivers and create outputs + for _, receiver := range receivers { + rcvAddr, err := common.DecodeAddress(receiver.To()) + if err != nil { + return "", fmt.Errorf("invalid receiver address: %s", err) + } + + rcvAspPubKey := schnorr.SerializePubKey(rcvAddr.Asp) + + if !bytes.Equal(expectedAspPubKey, rcvAspPubKey) { + return "", fmt.Errorf("invalid receiver address '%s': expected ASP %s, got %s", receiver.To(), hex.EncodeToString(expectedAspPubKey), hex.EncodeToString(rcvAspPubKey)) + } + + if receiver.Amount() < a.Dust { + return "", fmt.Errorf("invalid amount (%d), must be greater than dust %d", receiver.Amount(), a.Dust) + } + + outputs = append(outputs, client.Output{ + Address: receiver.To(), + Amount: receiver.Amount(), + }) + sumOfReceivers += receiver.Amount() + } + offchainAddrs, _, _, err := a.wallet.GetAddresses(ctx) if err != nil { return "", err } if len(offchainAddrs) <= 0 { - return "", fmt.Errorf("no funds detected") - } - - expectedAspPubkey := schnorr.SerializePubKey(a.AspPubkey) - - receiversOutput := make([]client.Output, 0) - sumOfReceivers := uint64(0) - - for _, receiver := range receivers { - rcvAddr, err := common.DecodeAddress(receiver.To()) - if err != nil { - return "", fmt.Errorf("invalid receiver address: %s", err) - } - - rcvAspPubkey := schnorr.SerializePubKey(rcvAddr.Asp) - - if !bytes.Equal(rcvAspPubkey, expectedAspPubkey) { - return "", fmt.Errorf("invalid receiver address '%s': expected ASP %s, got %s", receiver.To(), hex.EncodeToString(expectedAspPubkey), hex.EncodeToString(rcvAspPubkey)) - } - - if receiver.Amount() < a.Dust { - return "", fmt.Errorf("invalid amount (%d), must be greater than dust %d", receiver.Amount(), a.Dust) - } - - receiversOutput = append(receiversOutput, client.Output{ - Address: receiver.To(), - Amount: receiver.Amount(), - }) - sumOfReceivers += receiver.Amount() + return "", fmt.Errorf("no offchain addresses found") } vtxos := make([]client.DescriptorVtxo, 0) - for _, offchainAddr := range offchainAddrs { - spendableVtxos, err := a.getVtxos(ctx, offchainAddr.Address, withExpiryCoinselect) - if err != nil { - return "", err - } - for _, vtxo := range spendableVtxos { - vtxos = append(vtxos, client.DescriptorVtxo{ - Vtxo: vtxo, - Descriptor: offchainAddr.Descriptor, - }) + spendableVtxos, err := a.getVtxos(ctx, withExpiryCoinselect, nil) + if err != nil { + return "", err + } + + for _, offchainAddr := range offchainAddrs { + for _, v := range spendableVtxos { + vtxoAddr, err := v.Address(a.AspPubkey, a.Network) + if err != nil { + return "", err + } + + if vtxoAddr == offchainAddr.Address { + vtxos = append(vtxos, client.DescriptorVtxo{ + Vtxo: v, + Descriptor: offchainAddr.Descriptor, + }) + } } } - selectedCoins, changeAmount, err := utils.CoinSelect( - vtxos, sumOfReceivers, a.Dust, withExpiryCoinselect, - ) + boardingUtxos, err := a.getClaimableBoardingUtxos(ctx, nil) if err != nil { return "", err } + var selectedBoardingCoins []types.Utxo + var selectedCoins []client.DescriptorVtxo + var changeAmount uint64 + + // if no receivers, self send all selected coins + if len(outputs) <= 0 { + selectedBoardingCoins = boardingUtxos + selectedCoins = vtxos + + amount := uint64(0) + for _, utxo := range boardingUtxos { + amount += utxo.Amount + } + for _, utxo := range vtxos { + amount += utxo.Amount + } + + outputs = append(outputs, client.Output{ + Address: offchainAddrs[0].Address, + Amount: amount, + }) + + changeAmount = 0 + } else { + selectedBoardingCoins, selectedCoins, changeAmount, err = utils.CoinSelect( + boardingUtxos, vtxos, sumOfReceivers, a.Dust, withExpiryCoinselect, + ) + if err != nil { + return "", err + } + } + if changeAmount > 0 { offchainAddr, _, err := a.wallet.NewAddress(ctx, true) if err != nil { return "", err } - changeReceiver := client.Output{ + outputs = append(outputs, client.Output{ Address: offchainAddr.Address, Amount: changeAmount, - } - receiversOutput = append(receiversOutput, changeReceiver) + }) } - inputs := make([]client.Input, 0, len(selectedCoins)) + inputs := make([]client.Input, 0, len(selectedCoins)+len(selectedBoardingCoins)) + for _, coin := range selectedCoins { inputs = append(inputs, client.Input{ Outpoint: client.Outpoint{ @@ -993,16 +1003,23 @@ func (a *covenantArkClient) sendOffchain( Descriptor: coin.Descriptor, }) } + for _, coin := range selectedBoardingCoins { + inputs = append(inputs, client.Input{ + Outpoint: client.Outpoint{ + Txid: coin.Txid, + VOut: coin.VOut, + }, + Descriptor: coin.Descriptor, + }) + } - paymentID, err := a.client.RegisterInputsForNextRound( - ctx, inputs, "", // ephemeralPublicKey is not required for covenant - ) + paymentID, err := a.client.RegisterInputsForNextRound(ctx, inputs, "") if err != nil { return "", err } if err := a.client.RegisterOutputsForNextRound( - ctx, paymentID, receiversOutput, + ctx, paymentID, outputs, ); err != nil { return "", err } @@ -1010,7 +1027,7 @@ func (a *covenantArkClient) sendOffchain( log.Infof("payment registered with id: %s", paymentID) poolTxID, err := a.handleRoundStream( - ctx, paymentID, selectedCoins, nil, "", receiversOutput, + ctx, paymentID, selectedCoins, boardingUtxos, outputs, ) if err != nil { return "", err @@ -1023,7 +1040,7 @@ func (a *covenantArkClient) sendOffchain( func (a *covenantArkClient) addInputs( ctx context.Context, updater *psetv2.Updater, - utxos []explorer.Utxo, + utxos []types.Utxo, ) error { // TODO works only with single-key wallet offchain, _, err := a.wallet.NewAddress(ctx, false) @@ -1055,7 +1072,7 @@ func (a *covenantArkClient) addInputs( if err := updater.AddInputs([]psetv2.InputArgs{ { Txid: utxo.Txid, - TxIndex: utxo.Vout, + TxIndex: utxo.VOut, Sequence: sequence, }, }); err != nil { @@ -1113,8 +1130,7 @@ func (a *covenantArkClient) handleRoundStream( ctx context.Context, paymentID string, vtxosToSign []client.DescriptorVtxo, - boardingUtxos []explorer.Utxo, - boardingDescriptor string, + boardingUtxos []types.Utxo, receivers []client.Output, ) (string, error) { eventsCh, close, err := a.client.GetEventStream(ctx, paymentID) @@ -1151,7 +1167,7 @@ func (a *covenantArkClient) handleRoundStream( log.Info("a round finalization started") signedForfeitTxs, signedRoundTx, err := a.handleRoundFinalization( - ctx, event.(client.RoundFinalizationEvent), vtxosToSign, boardingUtxos, boardingDescriptor, receivers, + ctx, event.(client.RoundFinalizationEvent), vtxosToSign, boardingUtxos, receivers, ) if err != nil { return "", err @@ -1178,8 +1194,7 @@ func (a *covenantArkClient) handleRoundFinalization( ctx context.Context, event client.RoundFinalizationEvent, vtxos []client.DescriptorVtxo, - boardingUtxos []explorer.Utxo, - boardingDescriptor string, + boardingUtxos []types.Utxo, receivers []client.Output, ) (signedForfeits []string, signedRoundTx string, err error) { if err = a.validateCongestionTree(event, receivers); err != nil { @@ -1193,13 +1208,23 @@ func (a *covenantArkClient) handleRoundFinalization( } } - if len(boardingUtxos) > 0 { - boardingVtxoScript, err := tree.ParseVtxoScript(boardingDescriptor) - if err != nil { - return nil, "", err - } + // if no boarding utxos inputs, we don't need to sign the round transaction + if len(boardingUtxos) <= 0 { + return + } - roundPtx, err := psetv2.NewPsetFromBase64(event.Tx) + roundPtx, err := psetv2.NewPsetFromBase64(event.Tx) + if err != nil { + return nil, "", err + } + + updater, err := psetv2.NewUpdater(roundPtx) + if err != nil { + return nil, "", err + } + + for _, boardingUtxo := range boardingUtxos { + boardingVtxoScript, err := tree.ParseVtxoScript(boardingUtxo.Descriptor) if err != nil { return nil, "", err } @@ -1213,7 +1238,7 @@ func (a *covenantArkClient) handleRoundFinalization( AspPubkey: a.AspPubkey, } default: - return nil, "", fmt.Errorf("unsupported boarding descriptor: %s", boardingDescriptor) + return nil, "", fmt.Errorf("unsupported boarding descriptor: %s", boardingUtxo.Descriptor) } forfeitLeaf, err := forfeitClosure.Leaf() @@ -1241,31 +1266,25 @@ func (a *covenantArkClient) handleRoundFinalization( ControlBlock: *ctrlBlock, } - updater, err := psetv2.NewUpdater(roundPtx) - if err != nil { - return nil, "", err - } - for i, input := range updater.Pset.Inputs { - for _, boardingUtxo := range boardingUtxos { - if chainhash.Hash(input.PreviousTxid).String() == boardingUtxo.Txid && boardingUtxo.Vout == input.PreviousTxIndex { - if err := updater.AddInTapLeafScript(i, tapscript); err != nil { - return nil, "", err - } - break + if chainhash.Hash(input.PreviousTxid).String() == boardingUtxo.Txid && boardingUtxo.VOut == input.PreviousTxIndex { + if err := updater.AddInTapLeafScript(i, tapscript); err != nil { + return nil, "", err } + break } } - b64, err := updater.Pset.ToBase64() - if err != nil { - return nil, "", err - } + } - signedRoundTx, err = a.wallet.SignTransaction(ctx, a.explorer, b64) - if err != nil { - return nil, "", err - } + b64, err := updater.Pset.ToBase64() + if err != nil { + return nil, "", err + } + + signedRoundTx, err = a.wallet.SignTransaction(ctx, a.explorer, b64) + if err != nil { + return nil, "", err } return signedForfeits, signedRoundTx, nil @@ -1518,8 +1537,8 @@ func (a *covenantArkClient) createAndSignForfeits( } func (a *covenantArkClient) coinSelectOnchain( - ctx context.Context, targetAmount uint64, exclude []explorer.Utxo, -) ([]explorer.Utxo, uint64, error) { + ctx context.Context, targetAmount uint64, exclude []types.Utxo, +) ([]types.Utxo, uint64, error) { _, boardingAddrs, redemptionAddrs, err := a.wallet.GetAddresses(ctx) if err != nil { return nil, 0, err @@ -1527,7 +1546,7 @@ func (a *covenantArkClient) coinSelectOnchain( now := time.Now() - fetchedUtxos := make([]explorer.Utxo, 0) + fetchedUtxos := make([]types.Utxo, 0) for _, addr := range boardingAddrs { boardingDescriptor := addr.Descriptor @@ -1550,14 +1569,14 @@ func (a *covenantArkClient) coinSelectOnchain( } for _, utxo := range utxos { - u := utxo.ToUtxo(boardingTimeout) + u := utxo.ToUtxo(boardingTimeout, addr.Descriptor) if u.SpendableAt.Before(now) { fetchedUtxos = append(fetchedUtxos, u) } } } - selected := make([]explorer.Utxo, 0) + selected := make([]types.Utxo, 0) selectedAmount := uint64(0) for _, utxo := range fetchedUtxos { if selectedAmount >= targetAmount { @@ -1565,7 +1584,7 @@ func (a *covenantArkClient) coinSelectOnchain( } for _, excluded := range exclude { - if utxo.Txid == excluded.Txid && utxo.Vout == excluded.Vout { + if utxo.Txid == excluded.Txid && utxo.VOut == excluded.VOut { continue } } @@ -1578,7 +1597,7 @@ func (a *covenantArkClient) coinSelectOnchain( return selected, selectedAmount - targetAmount, nil } - fetchedUtxos = make([]explorer.Utxo, 0) + fetchedUtxos = make([]types.Utxo, 0) for _, addr := range redemptionAddrs { utxos, err := a.explorer.GetUtxos(addr.Address) if err != nil { @@ -1586,7 +1605,7 @@ func (a *covenantArkClient) coinSelectOnchain( } for _, utxo := range utxos { - u := utxo.ToUtxo(uint(a.UnilateralExitDelay)) + u := utxo.ToUtxo(uint(a.UnilateralExitDelay), addr.Descriptor) if u.SpendableAt.Before(now) { fetchedUtxos = append(fetchedUtxos, u) } @@ -1599,7 +1618,7 @@ func (a *covenantArkClient) coinSelectOnchain( } for _, excluded := range exclude { - if utxo.Txid == excluded.Txid && utxo.Vout == excluded.Vout { + if utxo.Txid == excluded.Txid && utxo.VOut == excluded.VOut { continue } } @@ -1648,11 +1667,11 @@ func (a *covenantArkClient) getRedeemBranches( } func (a *covenantArkClient) getOffchainBalance( - ctx context.Context, addr string, computeVtxoExpiration bool, + ctx context.Context, computeVtxoExpiration bool, ) (uint64, map[int64]uint64, error) { amountByExpiration := make(map[int64]uint64, 0) - vtxos, err := a.getVtxos(ctx, addr, computeVtxoExpiration) + vtxos, err := a.getVtxos(ctx, computeVtxoExpiration, nil) if err != nil { return 0, nil, err } @@ -1675,18 +1694,24 @@ func (a *covenantArkClient) getOffchainBalance( } func (a *covenantArkClient) getVtxos( - ctx context.Context, addr string, computeVtxoExpiration bool, + ctx context.Context, + withExpiryCoinselect bool, opts *CoinSelectOptions, ) ([]client.Vtxo, error) { - vtxos, _, err := a.client.ListVtxos(ctx, addr) + spendableVtxos, _, err := a.ListVtxos(ctx) if err != nil { return nil, err } - if !computeVtxoExpiration { - return vtxos, nil + if opts != nil && len(opts.OutpointsFilter) > 0 { + spendableVtxos = filterByOutpoints(spendableVtxos, opts.OutpointsFilter) } - redeemBranches, err := a.getRedeemBranches(ctx, vtxos) + if opts == nil || !opts.WithExpirySorting { + return spendableVtxos, nil + } + + // if sorting by expiry is required, we need to get the expiration date of each vtxo + redeemBranches, err := a.getRedeemBranches(ctx, spendableVtxos) if err != nil { return nil, err } @@ -1697,75 +1722,24 @@ func (a *covenantArkClient) getVtxos( return nil, err } - for i, vtxo := range vtxos { + for i, vtxo := range spendableVtxos { if vtxo.Txid == vtxoTxid { - vtxos[i].ExpiresAt = expiration + spendableVtxos[i].ExpiresAt = expiration break } } } - return vtxos, nil -} - -func (a *covenantArkClient) selfTransferAllPendingPayments( - ctx context.Context, boardingUtxos []explorer.Utxo, myself client.Output, boardingDescriptor string, -) (string, error) { - inputs := make([]client.Input, 0, len(boardingUtxos)) - - for _, utxo := range boardingUtxos { - inputs = append(inputs, client.Input{ - Outpoint: client.Outpoint{ - Txid: utxo.Txid, - VOut: utxo.Vout, - }, - Descriptor: boardingDescriptor, - }) - } - - outputs := []client.Output{myself} - - paymentID, err := a.client.RegisterInputsForNextRound(ctx, inputs, "") // ephemeralPublicKey is not required for covenant - if err != nil { - return "", err - } - - if err := a.client.RegisterOutputsForNextRound(ctx, paymentID, outputs); err != nil { - return "", err - } - - roundTxid, err := a.handleRoundStream( - ctx, paymentID, make([]client.DescriptorVtxo, 0), boardingUtxos, boardingDescriptor, outputs, - ) - if err != nil { - return "", err - } - - return roundTxid, nil + return spendableVtxos, nil } func (a *covenantArkClient) getBoardingTxs(ctx context.Context) (transactions []types.Transaction) { - utxos, err := a.getClaimableBoardingUtxos(ctx) - if err != nil { - return nil - } - - isPending := make(map[string]bool) - for _, u := range utxos { - isPending[u.Txid] = true - } - allUtxos, err := a.getAllBoardingUtxos(ctx) if err != nil { return nil } for _, u := range allUtxos { - pending := false - if isPending[u.Txid] { - pending = true - } - transactions = append(transactions, types.Transaction{ TransactionKey: types.TransactionKey{ BoardingTxid: u.Txid, @@ -1773,7 +1747,6 @@ func (a *covenantArkClient) getBoardingTxs(ctx context.Context) (transactions [] Amount: u.Amount, Type: types.TxReceived, CreatedAt: u.CreatedAt, - IsPending: pending, }) } return @@ -1796,9 +1769,7 @@ func vtxosToTxsCovenant( for _, v := range append(spendable, spent...) { // get vtxo amount amount := int(v.Amount) - if !v.Pending { - continue - } + // find other spent vtxos that spent this one relatedVtxos := findVtxosBySpentBy(spent, v.Txid) for _, r := range relatedVtxos { diff --git a/pkg/client-sdk/covenantless_client.go b/pkg/client-sdk/covenantless_client.go index a8d06e7..447f9c0 100644 --- a/pkg/client-sdk/covenantless_client.go +++ b/pkg/client-sdk/covenantless_client.go @@ -15,7 +15,6 @@ import ( "github.com/ark-network/ark/common/bitcointree" "github.com/ark-network/ark/common/tree" "github.com/ark-network/ark/pkg/client-sdk/client" - "github.com/ark-network/ark/pkg/client-sdk/explorer" "github.com/ark-network/ark/pkg/client-sdk/internal/utils" "github.com/ark-network/ark/pkg/client-sdk/redemption" "github.com/ark-network/ark/pkg/client-sdk/types" @@ -243,7 +242,7 @@ func (a *covenantlessArkClient) listenForTransactions(ctx context.Context) { continue } - pendingBoardingTxsMap, newPendingBoardingTxs, err := a.getBoardingPendingTransactions(ctx) + newPendingBoardingTxs, err := a.getBoardingPendingTransactions(ctx) if err != nil { log.WithError(err).Error("Failed to get pending transactions") continue @@ -255,7 +254,7 @@ func (a *covenantlessArkClient) listenForTransactions(ctx context.Context) { continue } - a.processTransactionEvent(addrPubkey, event, pendingBoardingTxsMap) + a.processTransactionEvent(addrPubkey, event) case <-ctx.Done(): return } @@ -269,7 +268,7 @@ func (a *covenantlessArkClient) listenForBoardingUtxos(ctx context.Context) { for { select { case <-ticker.C: - _, newPendingBoardingTxs, err := a.getBoardingPendingTransactions(ctx) + newPendingBoardingTxs, err := a.getBoardingPendingTransactions(ctx) if err != nil { log.WithError(err).Error("Failed to get pending transactions") continue @@ -288,58 +287,76 @@ func (a *covenantlessArkClient) listenForBoardingUtxos(ctx context.Context) { func (a *covenantlessArkClient) getBoardingPendingTransactions( ctx context.Context, -) (map[string]types.Transaction, []types.Transaction, error) { +) ([]types.Transaction, error) { oldTxs, err := a.store.TransactionStore().GetAllTransactions(ctx) if err != nil { - return nil, nil, err + return nil, err } - pendingBoardingTxsMap := make(map[string]types.Transaction) - for _, tx := range oldTxs { - if tx.IsBoarding() { - if tx.IsPending { - pendingBoardingTxsMap[tx.BoardingTxid] = tx - } - } - } - - boardingTxs, _, err := a.getBoardingTxs(ctx) + boardingUtxos, err := a.getClaimableBoardingUtxos(ctx, nil) if err != nil { - return nil, nil, err - } - newPendingBoardingTxs := make([]types.Transaction, 0) - for _, tx := range boardingTxs { - if tx.IsBoarding() && tx.IsPending { - if _, ok := pendingBoardingTxsMap[tx.BoardingTxid]; !ok { - newPendingBoardingTxs = append(newPendingBoardingTxs, tx) - pendingBoardingTxsMap[tx.BoardingTxid] = tx - } - } + return nil, err } - return pendingBoardingTxsMap, newPendingBoardingTxs, nil + newPendingBoardingTxs := make([]types.Transaction, 0) + for _, u := range boardingUtxos { + found := false + for _, tx := range oldTxs { + if tx.BoardingTxid == u.Txid { + found = true + break + } + } + + if found { + continue + } + + newPendingBoardingTxs = append(newPendingBoardingTxs, types.Transaction{ + TransactionKey: types.TransactionKey{ + BoardingTxid: u.Txid, + }, + Amount: u.Amount, + Type: types.TxReceived, + CreatedAt: u.CreatedAt, + }) + } + + return newPendingBoardingTxs, nil } func (a *covenantlessArkClient) processTransactionEvent( pubkey string, event client.TransactionEvent, - pendingBoardingTxsMap map[string]types.Transaction, ) { if event.Round != nil { - boardingAmount := 0 - boardingTxsToUpdate := make([]types.Transaction, 0) - for _, v := range event.Round.ClaimedBoardingUtxos { - boardingKey := fmt.Sprintf("%v-%v", v.Txid, v.VOut) - if tx, ok := pendingBoardingTxsMap[boardingKey]; ok { - boardingAmount += int(tx.Amount) - tx.IsPending = false - boardingTxsToUpdate = append(boardingTxsToUpdate, tx) + allTxs, err := a.store.TransactionStore().GetAllTransactions(context.Background()) + if err != nil { + log.WithError(err).Error("Failed to get all transactions") + return + } + pendingBoardingTxs := make(map[string]types.Transaction) + for _, tx := range allTxs { + if tx.BoardingTxid != "" && !tx.Settled { + pendingBoardingTxs[tx.BoardingTxid] = tx + } + } + var ignoreNewTxs bool + settledBoardingTxs := make([]types.Transaction, 0, len(event.Round.ClaimedBoardingUtxos)) + for _, u := range event.Round.ClaimedBoardingUtxos { + if tx, ok := pendingBoardingTxs[u.Txid]; ok { + ignoreNewTxs = true + tx.Settled = true + settledBoardingTxs = append(settledBoardingTxs, tx) } } - if err := a.store.TransactionStore(). - UpdateTransactions(context.Background(), boardingTxsToUpdate); err != nil { - log.WithError(err).Error("Failed to update boarding transactions") + if len(settledBoardingTxs) > 0 { + if err := a.store.TransactionStore(). + UpdateTransactions(context.Background(), settledBoardingTxs); err != nil { + log.WithError(err).Error("Failed to settle boarding transactions") + return + } } spentKeys := make([]types.VtxoKey, 0, len(event.Round.SpentVtxos)) @@ -388,33 +405,34 @@ func (a *covenantlessArkClient) processTransactionEvent( Spent: false, }) - if boardingAmount == int(v.Amount) { - continue + if !ignoreNewTxs { + txsToInsert = append(txsToInsert, types.Transaction{ + TransactionKey: types.TransactionKey{ + RoundTxid: event.Round.Txid, + }, + Amount: v.Amount, + Type: types.TxReceived, + CreatedAt: time.Now(), //TODO is this ok? + }) } - - txsToInsert = append(txsToInsert, types.Transaction{ - TransactionKey: types.TransactionKey{ - RoundTxid: event.Round.Txid, - }, - Amount: v.Amount, - Type: types.TxReceived, - CreatedAt: time.Now(), //TODO is this ok? - }) } } - if err := a.store.VtxoStore(). - AddVtxos(context.Background(), vtxosToInsert); err != nil { - log.WithError(err).Error("Failed to insert new vtxos") - return + if len(vtxosToInsert) > 0 { + if err := a.store.VtxoStore(). + AddVtxos(context.Background(), vtxosToInsert); err != nil { + log.WithError(err).Error("Failed to insert new vtxos") + return + } } - if err := a.store.TransactionStore(). - AddTransactions(context.Background(), txsToInsert); err != nil { - log.WithError(err).Error("Failed to insert received transaction") - return + if len(txsToInsert) > 0 { + if err := a.store.TransactionStore(). + AddTransactions(context.Background(), txsToInsert); err != nil { + log.WithError(err).Error("Failed to insert received transaction") + return + } } - } if event.Redeem != nil { @@ -474,7 +492,6 @@ func (a *covenantlessArkClient) processTransactionEvent( }, Amount: inputAmount - outputAmount, Type: types.TxSent, - IsPending: true, CreatedAt: time.Now(), //TODO is this ok? } @@ -504,7 +521,6 @@ func (a *covenantlessArkClient) processTransactionEvent( }, Amount: v.Amount, Type: types.TxReceived, - IsPending: true, CreatedAt: time.Now(), //TODO is this ok? } if err := a.store.TransactionStore(). @@ -523,80 +539,6 @@ func (a *covenantlessArkClient) processTransactionEvent( } } -func (a *covenantlessArkClient) ListVtxos( - ctx context.Context, -) (spendableVtxos, spentVtxos []client.Vtxo, err error) { - offchainAddrs, boardingAddrs, _, err := a.wallet.GetAddresses(ctx) - if err != nil { - return - } - - boardingAddrScript, err := bitcointree.ParseVtxoScript(boardingAddrs[0].Descriptor) - if err != nil { - return - } - - var myPubkey []byte - - if boardingScript, ok := boardingAddrScript.(*bitcointree.DefaultVtxoScript); ok { - myPubkey = schnorr.SerializePubKey(boardingScript.Owner) - } - - if myPubkey == nil { - return nil, nil, fmt.Errorf("invalid boarding address descriptor") - } - - // The ASP returns the vtxos sent to others via async payments as spendable - // because they are actually revertable. Since we do not provide any revert - // feature, we want to ignore them. - // To understand if the output of a redeem tx is sent or received we look at - // the inputs and check if they are owned by us. - // The auxiliary variables below are used to make these checks in an - // efficient way. - for _, addr := range offchainAddrs { - spendable, spent, err := a.client.ListVtxos(ctx, addr.Address) - if err != nil { - return nil, nil, err - } - - script, err := bitcointree.ParseVtxoScript(addr.Descriptor) - if err != nil { - return nil, nil, err - } - - reversibleVtxo, isReversible := script.(*bitcointree.ReversibleVtxoScript) - - for _, v := range spendable { - if !v.Pending { - spendableVtxos = append(spendableVtxos, v) - continue - } - - if !isReversible { - spendableVtxos = append(spendableVtxos, v) - continue - } - - if !bytes.Equal(schnorr.SerializePubKey(reversibleVtxo.Sender), myPubkey) { - spendableVtxos = append(spendableVtxos, v) - } - } - - for _, v := range spent { - if !isReversible { - spentVtxos = append(spentVtxos, v) - continue - } - - if !bytes.Equal(schnorr.SerializePubKey(reversibleVtxo.Sender), myPubkey) { - spentVtxos = append(spentVtxos, v) - } - } - } - - return -} - func (a *covenantlessArkClient) Balance( ctx context.Context, computeVtxoExpiration bool, ) (*Balance, error) { @@ -773,7 +715,7 @@ func (a *covenantlessArkClient) UnilateralRedeem(ctx context.Context) error { return fmt.Errorf("wallet is locked") } - vtxos, _, err := a.getVtxos(ctx, false) + vtxos, err := a.getVtxos(ctx, nil) if err != nil { return err } @@ -853,7 +795,7 @@ func (a *covenantlessArkClient) CollaborativeRedeem( } vtxos := make([]client.DescriptorVtxo, 0) - spendableVtxos, _, err := a.getVtxos(ctx, withExpiryCoinselect) + spendableVtxos, err := a.getVtxos(ctx, nil) if err != nil { return "", err } @@ -874,8 +816,13 @@ func (a *covenantlessArkClient) CollaborativeRedeem( } } - selectedCoins, changeAmount, err := utils.CoinSelect( - vtxos, amount, a.Dust, withExpiryCoinselect, + boardingUtxos, err := a.getClaimableBoardingUtxos(ctx, nil) + if err != nil { + return "", err + } + + selectedBoardingCoins, selectedCoins, changeAmount, err := utils.CoinSelect( + boardingUtxos, vtxos, amount, a.Dust, withExpiryCoinselect, ) if err != nil { return "", err @@ -893,7 +840,7 @@ func (a *covenantlessArkClient) CollaborativeRedeem( }) } - inputs := make([]client.Input, 0, len(selectedCoins)) + inputs := make([]client.Input, 0, len(selectedCoins)+len(selectedBoardingCoins)) for _, coin := range selectedCoins { inputs = append(inputs, client.Input{ @@ -904,6 +851,15 @@ func (a *covenantlessArkClient) CollaborativeRedeem( Descriptor: coin.Descriptor, }) } + for _, coin := range selectedBoardingCoins { + inputs = append(inputs, client.Input{ + Outpoint: client.Outpoint{ + Txid: coin.Txid, + VOut: coin.VOut, + }, + Descriptor: coin.Descriptor, + }) + } roundEphemeralKey, err := secp256k1.GeneratePrivateKey() if err != nil { @@ -924,7 +880,7 @@ func (a *covenantlessArkClient) CollaborativeRedeem( } poolTxID, err := a.handleRoundStream( - ctx, paymentID, selectedCoins, nil, "", receivers, roundEphemeralKey, + ctx, paymentID, selectedCoins, selectedBoardingCoins, receivers, roundEphemeralKey, ) if err != nil { return "", err @@ -986,8 +942,10 @@ func (a *covenantlessArkClient) SendAsync( } vtxos := make([]client.DescriptorVtxo, 0) - - spendableVtxos, _, err := a.getVtxos(ctx, withExpiryCoinselect) + opts := &CoinSelectOptions{ + WithExpirySorting: withExpiryCoinselect, + } + spendableVtxos, err := a.getVtxos(ctx, opts) if err != nil { return "", err } @@ -1008,8 +966,9 @@ func (a *covenantlessArkClient) SendAsync( } } - selectedCoins, changeAmount, err := utils.CoinSelect( - vtxos, sumOfReceivers, a.Dust, withExpiryCoinselect, + // do not include boarding utxos + _, selectedCoins, changeAmount, err := utils.CoinSelect( + nil, vtxos, sumOfReceivers, a.Dust, withExpiryCoinselect, ) if err != nil { return "", err @@ -1039,11 +998,6 @@ func (a *covenantlessArkClient) SendAsync( Pubkey: s.Owner, AspPubkey: s.Asp, } - case *bitcointree.ReversibleVtxoScript: - forfeitClosure = &bitcointree.MultisigClosure{ - Pubkey: s.Owner, - AspPubkey: s.Asp, - } default: return "", fmt.Errorf("unsupported vtxo script: %T", s) } @@ -1086,53 +1040,8 @@ func (a *covenantlessArkClient) SendAsync( return signedRedeemTx, nil } -func (a *covenantlessArkClient) Claim(ctx context.Context) (string, error) { - myselfOffchain, boardingAddr, err := a.wallet.NewAddress(ctx, false) - if err != nil { - return "", err - } - - _, pendingVtxos, err := a.getVtxos(ctx, false) - if err != nil { - return "", err - } - - pendingVtxosWithDescriptor := make([]client.DescriptorVtxo, 0) - for _, vtxo := range pendingVtxos { - pendingVtxosWithDescriptor = append(pendingVtxosWithDescriptor, client.DescriptorVtxo{ - Vtxo: vtxo, - Descriptor: myselfOffchain.Descriptor, - }) - } - - boardingUtxos, err := a.getClaimableBoardingUtxos(ctx) - if err != nil { - return "", err - } - - var pendingBalance uint64 - for _, vtxo := range pendingVtxos { - pendingBalance += vtxo.Amount - } - for _, vtxo := range boardingUtxos { - pendingBalance += vtxo.Amount - } - if pendingBalance == 0 { - return "", nil - } - - receiver := client.Output{ - Address: myselfOffchain.Address, - Amount: pendingBalance, - } - - return a.selfTransferAllPendingPayments( - ctx, - pendingVtxosWithDescriptor, - boardingUtxos, - receiver, - boardingAddr.Descriptor, - ) +func (a *covenantlessArkClient) Settle(ctx context.Context) (string, error) { + return a.sendOffchain(ctx, false, nil) } func (a *covenantlessArkClient) GetTransactionHistory( @@ -1326,19 +1235,11 @@ func (a *covenantlessArkClient) sendOffchain( return "", fmt.Errorf("wallet is locked") } - offchainAddrs, _, _, err := a.wallet.GetAddresses(ctx) - if err != nil { - return "", err - } - if len(offchainAddrs) <= 0 { - return "", fmt.Errorf("no funds detected") - } - expectedAspPubKey := schnorr.SerializePubKey(a.AspPubkey) - - receiversOutput := make([]client.Output, 0) + outputs := make([]client.Output, 0) sumOfReceivers := uint64(0) + // validate receivers and create outputs for _, receiver := range receivers { rcvAddr, err := common.DecodeAddress(receiver.To()) if err != nil { @@ -1355,16 +1256,25 @@ func (a *covenantlessArkClient) sendOffchain( return "", fmt.Errorf("invalid amount (%d), must be greater than dust %d", receiver.Amount(), a.Dust) } - receiversOutput = append(receiversOutput, client.Output{ + outputs = append(outputs, client.Output{ Address: receiver.To(), Amount: receiver.Amount(), }) sumOfReceivers += receiver.Amount() } - vtxos := make([]client.DescriptorVtxo, 0) + offchainAddrs, _, _, err := a.wallet.GetAddresses(ctx) + if err != nil { + return "", err + } + if len(offchainAddrs) <= 0 { + return "", fmt.Errorf("no offchain addresses found") + } - spendableVtxos, _, err := a.getVtxos(ctx, withExpiryCoinselect) + vtxos := make([]client.DescriptorVtxo, 0) + opts := &CoinSelectOptions{ + WithExpirySorting: withExpiryCoinselect} + spendableVtxos, err := a.getVtxos(ctx, opts) if err != nil { return "", err } @@ -1385,26 +1295,55 @@ func (a *covenantlessArkClient) sendOffchain( } } - selectedCoins, changeAmount, err := utils.CoinSelect( - vtxos, sumOfReceivers, a.Dust, withExpiryCoinselect, - ) + boardingUtxos, err := a.getClaimableBoardingUtxos(ctx, nil) if err != nil { return "", err } + var selectedBoardingCoins []types.Utxo + var selectedCoins []client.DescriptorVtxo + var changeAmount uint64 + + // if no receivers, self send all selected coins + if len(outputs) <= 0 { + selectedBoardingCoins = boardingUtxos + selectedCoins = vtxos + + amount := uint64(0) + for _, utxo := range boardingUtxos { + amount += utxo.Amount + } + for _, utxo := range vtxos { + amount += utxo.Amount + } + + outputs = append(outputs, client.Output{ + Address: offchainAddrs[0].Address, + Amount: amount, + }) + + changeAmount = 0 + } else { + selectedBoardingCoins, selectedCoins, changeAmount, err = utils.CoinSelect( + boardingUtxos, vtxos, sumOfReceivers, a.Dust, withExpiryCoinselect, + ) + if err != nil { + return "", err + } + } + if changeAmount > 0 { offchainAddr, _, err := a.wallet.NewAddress(ctx, true) if err != nil { return "", err } - changeReceiver := client.Output{ + outputs = append(outputs, client.Output{ Address: offchainAddr.Address, Amount: changeAmount, - } - receiversOutput = append(receiversOutput, changeReceiver) + }) } - inputs := make([]client.Input, 0, len(selectedCoins)) + inputs := make([]client.Input, 0, len(selectedCoins)+len(selectedBoardingCoins)) for _, coin := range selectedCoins { inputs = append(inputs, client.Input{ Outpoint: client.Outpoint{ @@ -1414,6 +1353,15 @@ func (a *covenantlessArkClient) sendOffchain( Descriptor: coin.Descriptor, }) } + for _, boardingUtxo := range selectedBoardingCoins { + inputs = append(inputs, client.Input{ + Outpoint: client.Outpoint{ + Txid: boardingUtxo.Txid, + VOut: boardingUtxo.VOut, + }, + Descriptor: boardingUtxo.Descriptor, + }) + } roundEphemeralKey, err := secp256k1.GeneratePrivateKey() if err != nil { @@ -1428,7 +1376,7 @@ func (a *covenantlessArkClient) sendOffchain( } if err := a.client.RegisterOutputsForNextRound( - ctx, paymentID, receiversOutput, + ctx, paymentID, outputs, ); err != nil { return "", err } @@ -1436,7 +1384,7 @@ func (a *covenantlessArkClient) sendOffchain( log.Infof("payment registered with id: %s", paymentID) poolTxID, err := a.handleRoundStream( - ctx, paymentID, selectedCoins, nil, "", receiversOutput, roundEphemeralKey, + ctx, paymentID, selectedCoins, selectedBoardingCoins, outputs, roundEphemeralKey, ) if err != nil { return "", err @@ -1448,7 +1396,7 @@ func (a *covenantlessArkClient) sendOffchain( func (a *covenantlessArkClient) addInputs( ctx context.Context, updater *psbt.Updater, - utxos []explorer.Utxo, + utxos []types.Utxo, ) error { // TODO works only with single-key wallet offchain, _, err := a.wallet.NewAddress(ctx, false) @@ -1485,7 +1433,7 @@ func (a *covenantlessArkClient) addInputs( updater.Upsbt.UnsignedTx.AddTxIn(&wire.TxIn{ PreviousOutPoint: wire.OutPoint{ Hash: *previousHash, - Index: utxo.Vout, + Index: utxo.VOut, }, Sequence: sequence, }) @@ -1534,8 +1482,7 @@ func (a *covenantlessArkClient) handleRoundStream( ctx context.Context, paymentID string, vtxosToSign []client.DescriptorVtxo, - boardingUtxos []explorer.Utxo, - boardingDescriptor string, + boardingUtxos []types.Utxo, receivers []client.Output, roundEphemeralKey *secp256k1.PrivateKey, ) (string, error) { @@ -1616,7 +1563,7 @@ func (a *covenantlessArkClient) handleRoundStream( log.Info("a round finalization started") signedForfeitTxs, signedRoundTx, err := a.handleRoundFinalization( - ctx, event.(client.RoundFinalizationEvent), vtxosToSign, boardingUtxos, boardingDescriptor, receivers, + ctx, event.(client.RoundFinalizationEvent), vtxosToSign, boardingUtxos, receivers, ) if err != nil { return "", err @@ -1720,8 +1667,7 @@ func (a *covenantlessArkClient) handleRoundFinalization( ctx context.Context, event client.RoundFinalizationEvent, vtxos []client.DescriptorVtxo, - boardingUtxos []explorer.Utxo, - boardingDescriptor string, + boardingUtxos []types.Utxo, receivers []client.Output, ) ([]string, string, error) { if err := a.validateCongestionTree(event, receivers); err != nil { @@ -1741,13 +1687,18 @@ func (a *covenantlessArkClient) handleRoundFinalization( forfeits = signedForfeits } - if len(boardingUtxos) > 0 { - boardingVtxoScript, err := bitcointree.ParseVtxoScript(boardingDescriptor) - if err != nil { - return nil, "", err - } + // if no boarding utxos inputs, we don't need to sign the round transaction + if len(boardingUtxos) <= 0 { + return forfeits, "", nil + } - roundPtx, err := psbt.NewFromRawBytes(strings.NewReader(event.Tx), true) + roundPtx, err := psbt.NewFromRawBytes(strings.NewReader(event.Tx), true) + if err != nil { + return nil, "", err + } + + for _, boardingUtxo := range boardingUtxos { + boardingVtxoScript, err := bitcointree.ParseVtxoScript(boardingUtxo.Descriptor) if err != nil { return nil, "", err } @@ -1758,7 +1709,7 @@ func (a *covenantlessArkClient) handleRoundFinalization( case *bitcointree.DefaultVtxoScript: myPubkey = v.Owner default: - return nil, "", fmt.Errorf("unsupported boarding descriptor: %s", boardingDescriptor) + return nil, "", fmt.Errorf("unsupported boarding descriptor: %s", boardingUtxo.Descriptor) } // add tapscript leaf @@ -1791,28 +1742,24 @@ func (a *covenantlessArkClient) handleRoundFinalization( for i := range roundPtx.Inputs { previousOutpoint := roundPtx.UnsignedTx.TxIn[i].PreviousOutPoint - for _, boardingUtxo := range boardingUtxos { - if boardingUtxo.Txid == previousOutpoint.Hash.String() && boardingUtxo.Vout == previousOutpoint.Index { - roundPtx.Inputs[i].TaprootLeafScript = []*psbt.TaprootTapLeafScript{tapscript} - break - } + if boardingUtxo.Txid == previousOutpoint.Hash.String() && boardingUtxo.VOut == previousOutpoint.Index { + roundPtx.Inputs[i].TaprootLeafScript = []*psbt.TaprootTapLeafScript{tapscript} + break } } - - b64, err := roundPtx.B64Encode() - if err != nil { - return nil, "", err - } - - signedRoundTx, err := a.wallet.SignTransaction(ctx, a.explorer, b64) - if err != nil { - return nil, "", err - } - - return forfeits, signedRoundTx, nil } - return forfeits, "", nil + b64, err := roundPtx.B64Encode() + if err != nil { + return nil, "", err + } + + signedRoundTx, err := a.wallet.SignTransaction(ctx, a.explorer, b64) + if err != nil { + return nil, "", err + } + + return forfeits, signedRoundTx, nil } func (a *covenantlessArkClient) validateCongestionTree( @@ -2021,11 +1968,6 @@ func (a *covenantlessArkClient) createAndSignForfeits( Pubkey: v.Owner, AspPubkey: a.AspPubkey, } - case *bitcointree.ReversibleVtxoScript: - forfeitClosure = &bitcointree.MultisigClosure{ - Pubkey: v.Owner, - AspPubkey: a.AspPubkey, - } default: return nil, fmt.Errorf("unsupported vtxo script: %T", vtxoScript) } @@ -2081,8 +2023,8 @@ func (a *covenantlessArkClient) createAndSignForfeits( } func (a *covenantlessArkClient) coinSelectOnchain( - ctx context.Context, targetAmount uint64, exclude []explorer.Utxo, -) ([]explorer.Utxo, uint64, error) { + ctx context.Context, targetAmount uint64, exclude []types.Utxo, +) ([]types.Utxo, uint64, error) { _, boardingAddrs, redemptionAddrs, err := a.wallet.GetAddresses(ctx) if err != nil { return nil, 0, err @@ -2090,7 +2032,7 @@ func (a *covenantlessArkClient) coinSelectOnchain( now := time.Now() - fetchedUtxos := make([]explorer.Utxo, 0) + fetchedUtxos := make([]types.Utxo, 0) for _, addr := range boardingAddrs { boardingScript, err := bitcointree.ParseVtxoScript(addr.Descriptor) if err != nil { @@ -2110,14 +2052,14 @@ func (a *covenantlessArkClient) coinSelectOnchain( } for _, utxo := range utxos { - u := utxo.ToUtxo(boardingTimeout) + u := utxo.ToUtxo(boardingTimeout, addr.Descriptor) if u.SpendableAt.Before(now) { fetchedUtxos = append(fetchedUtxos, u) } } } - selected := make([]explorer.Utxo, 0) + selected := make([]types.Utxo, 0) selectedAmount := uint64(0) for _, utxo := range fetchedUtxos { if selectedAmount >= targetAmount { @@ -2125,7 +2067,7 @@ func (a *covenantlessArkClient) coinSelectOnchain( } for _, excluded := range exclude { - if utxo.Txid == excluded.Txid && utxo.Vout == excluded.Vout { + if utxo.Txid == excluded.Txid && utxo.VOut == excluded.VOut { continue } } @@ -2138,7 +2080,7 @@ func (a *covenantlessArkClient) coinSelectOnchain( return selected, selectedAmount - targetAmount, nil } - fetchedUtxos = make([]explorer.Utxo, 0) + fetchedUtxos = make([]types.Utxo, 0) for _, addr := range redemptionAddrs { utxos, err := a.explorer.GetUtxos(addr.Address) if err != nil { @@ -2146,7 +2088,7 @@ func (a *covenantlessArkClient) coinSelectOnchain( } for _, utxo := range utxos { - u := utxo.ToUtxo(uint(a.UnilateralExitDelay)) + u := utxo.ToUtxo(uint(a.UnilateralExitDelay), addr.Descriptor) if u.SpendableAt.Before(now) { fetchedUtxos = append(fetchedUtxos, u) } @@ -2159,7 +2101,7 @@ func (a *covenantlessArkClient) coinSelectOnchain( } for _, excluded := range exclude { - if utxo.Txid == excluded.Txid && utxo.Vout == excluded.Vout { + if utxo.Txid == excluded.Txid && utxo.VOut == excluded.VOut { continue } } @@ -2213,15 +2155,14 @@ func (a *covenantlessArkClient) getRedeemBranches( return redeemBranches, nil } -// TODO (@louisinger): return pending balance in dedicated map. -// Currently, the returned balance is calculated from both spendable and -// pending vtxos. func (a *covenantlessArkClient) getOffchainBalance( ctx context.Context, computeVtxoExpiration bool, ) (uint64, map[int64]uint64, error) { amountByExpiration := make(map[int64]uint64, 0) - - vtxos, _, err := a.getVtxos(ctx, computeVtxoExpiration) + opts := &CoinSelectOptions{ + WithExpirySorting: computeVtxoExpiration, + } + vtxos, err := a.getVtxos(ctx, opts) if err != nil { return 0, nil, err } @@ -2245,13 +2186,13 @@ func (a *covenantlessArkClient) getOffchainBalance( func (a *covenantlessArkClient) getAllBoardingUtxos( ctx context.Context, -) ([]explorer.Utxo, map[string]struct{}, error) { +) ([]types.Utxo, map[string]struct{}, error) { _, boardingAddrs, _, err := a.wallet.GetAddresses(ctx) if err != nil { return nil, nil, err } - utxos := []explorer.Utxo{} + utxos := []types.Utxo{} ignoreVtxos := make(map[string]struct{}, 0) for _, addr := range boardingAddrs { txs, err := a.explorer.GetTxs(addr.Address) @@ -2260,6 +2201,7 @@ func (a *covenantlessArkClient) getAllBoardingUtxos( } for _, tx := range txs { for i, vout := range tx.Vout { + var spent bool if vout.Address == addr.Address { spentStatuses, err := a.explorer.GetTxOutspends(tx.Txid) if err != nil { @@ -2267,16 +2209,19 @@ func (a *covenantlessArkClient) getAllBoardingUtxos( } if s := spentStatuses[i]; s.Spent { ignoreVtxos[s.SpentBy] = struct{}{} + spent = true } createdAt := time.Time{} if tx.Status.Confirmed { createdAt = time.Unix(tx.Status.Blocktime, 0) } - utxos = append(utxos, explorer.Utxo{ - Txid: tx.Txid, - Vout: uint32(i), - Amount: vout.Amount, - CreatedAt: createdAt, + utxos = append(utxos, types.Utxo{ + Txid: tx.Txid, + VOut: uint32(i), + Amount: vout.Amount, + CreatedAt: createdAt, + Descriptor: addr.Descriptor, + Spent: spent, }) } } @@ -2286,14 +2231,13 @@ func (a *covenantlessArkClient) getAllBoardingUtxos( return utxos, ignoreVtxos, nil } -func (a *covenantlessArkClient) getClaimableBoardingUtxos(ctx context.Context) ([]explorer.Utxo, error) { +func (a *covenantlessArkClient) getClaimableBoardingUtxos(ctx context.Context, opts *CoinSelectOptions) ([]types.Utxo, error) { _, boardingAddrs, _, err := a.wallet.GetAddresses(ctx) if err != nil { return nil, err } - claimable := make([]explorer.Utxo, 0) - now := time.Now() + claimable := make([]types.Utxo, 0) for _, addr := range boardingAddrs { boardingScript, err := bitcointree.ParseVtxoScript(addr.Descriptor) @@ -2314,47 +2258,65 @@ func (a *covenantlessArkClient) getClaimableBoardingUtxos(ctx context.Context) ( return nil, err } + now := time.Now() + for _, utxo := range boardingUtxos { - u := utxo.ToUtxo(boardingTimeout) + if opts != nil && len(opts.OutpointsFilter) > 0 { + utxoOutpoint := client.Outpoint{ + Txid: utxo.Txid, + VOut: utxo.Vout, + } + found := false + for _, outpoint := range opts.OutpointsFilter { + if outpoint.Equals(utxoOutpoint) { + found = true + break + } + } + + if !found { + continue + } + } + + u := utxo.ToUtxo(boardingTimeout, addr.Descriptor) if u.SpendableAt.Before(now) { continue } + claimable = append(claimable, u) } - } return claimable, nil } func (a *covenantlessArkClient) getVtxos( - ctx context.Context, computeVtxoExpiration bool, -) ([]client.Vtxo, []client.Vtxo, error) { + ctx context.Context, opts *CoinSelectOptions, +) ([]client.Vtxo, error) { spendableVtxos, _, err := a.ListVtxos(ctx) if err != nil { - return nil, nil, err + return nil, err } - pendingVtxos := make([]client.Vtxo, 0) - for _, vtxo := range spendableVtxos { - if vtxo.RedeemTx != "" { - pendingVtxos = append(pendingVtxos, vtxo) - } + if opts != nil && len(opts.OutpointsFilter) > 0 { + spendableVtxos = filterByOutpoints(spendableVtxos, opts.OutpointsFilter) } - if !computeVtxoExpiration { - return spendableVtxos, pendingVtxos, nil + if opts == nil || !opts.WithExpirySorting { + return spendableVtxos, nil } + // if sorting by expiry is required, we need to get the expiration date of each vtxo redeemBranches, err := a.getRedeemBranches(ctx, spendableVtxos) if err != nil { - return nil, nil, err + return nil, err } for vtxoTxid, branch := range redeemBranches { expiration, err := branch.ExpiresAt() if err != nil { - return nil, nil, err + return nil, err } for i, vtxo := range spendableVtxos { @@ -2365,83 +2327,12 @@ func (a *covenantlessArkClient) getVtxos( } } - return spendableVtxos, pendingVtxos, nil + return spendableVtxos, nil } -func (a *covenantlessArkClient) selfTransferAllPendingPayments( - ctx context.Context, pendingVtxos []client.DescriptorVtxo, boardingUtxos []explorer.Utxo, myself client.Output, boardingDescriptor string, -) (string, error) { - inputs := make([]client.Input, 0, len(pendingVtxos)+len(boardingUtxos)) - - for _, coin := range pendingVtxos { - inputs = append(inputs, client.Input{ - Outpoint: client.Outpoint{ - Txid: coin.Txid, - VOut: coin.VOut, - }, - Descriptor: coin.Descriptor, - }) - } - - for _, utxo := range boardingUtxos { - inputs = append(inputs, client.Input{ - Outpoint: client.Outpoint{ - Txid: utxo.Txid, - VOut: utxo.Vout, - }, - Descriptor: boardingDescriptor, - }) - } - outputs := []client.Output{myself} - - roundEphemeralKey, err := secp256k1.GeneratePrivateKey() - if err != nil { - return "", err - } - - paymentID, err := a.client.RegisterInputsForNextRound( - ctx, - inputs, - hex.EncodeToString(roundEphemeralKey.PubKey().SerializeCompressed()), - ) - if err != nil { - return "", err - } - - if err := a.client.RegisterOutputsForNextRound(ctx, paymentID, outputs); err != nil { - return "", err - } - - roundTxid, err := a.handleRoundStream( - ctx, paymentID, pendingVtxos, boardingUtxos, boardingDescriptor, outputs, roundEphemeralKey, - ) - if err != nil { - return "", err - } - - return roundTxid, nil -} - -// getBoardingTxs builds the boarding tx history from onchain utxos: -// - unspent utxo => pending boarding tx -// - spent utxo => claimed boarding tx -// -// The tx spending an onchain utxo is an ark round, therefore an indexed list -// of round txids is returned to specify the vtxos to be ignored to build the -// offchain tx history and prevent duplicates. func (a *covenantlessArkClient) getBoardingTxs( ctx context.Context, ) ([]types.Transaction, map[string]struct{}, error) { - utxos, err := a.getClaimableBoardingUtxos(ctx) - if err != nil { - return nil, nil, err - } - - isPending := make(map[string]bool) - for _, u := range utxos { - isPending[u.Txid] = true - } - allUtxos, ignoreVtxos, err := a.getAllBoardingUtxos(ctx) if err != nil { return nil, nil, err @@ -2450,19 +2341,14 @@ func (a *covenantlessArkClient) getBoardingTxs( unconfirmedTxs := make([]types.Transaction, 0) confirmedTxs := make([]types.Transaction, 0) for _, u := range allUtxos { - pending := false - if isPending[u.Txid] { - pending = true - } - tx := types.Transaction{ TransactionKey: types.TransactionKey{ BoardingTxid: u.Txid, }, Amount: u.Amount, Type: types.TxReceived, - IsPending: pending, CreatedAt: u.CreatedAt, + Settled: u.Spent, } emptyTime := time.Time{} @@ -2490,23 +2376,37 @@ func vtxosToTxsCovenantless( roundLifetime int64, spendable, spent []client.Vtxo, ignoreVtxos map[string]struct{}, ) ([]types.Transaction, error) { transactions := make([]types.Transaction, 0) - indexedTxs := make(map[string]types.Transaction) + settledVtxos := make(map[string]struct{}) + + // First, loop over all vtxos to find those that have been settled + // (they have round txid instead of redeem tx) + for _, v := range append(spendable, spent...) { + _, ok1 := ignoreVtxos[v.Txid] + _, ok2 := ignoreVtxos[v.RoundTxid] + if ok1 || ok2 { + continue + } + if len(v.RoundTxid) > 0 { + settledVtxos[v.RoundTxid] = struct{}{} + } + } + for _, v := range spent { - // If the vtxo was pending and is spent => it's been claimed. - if v.Pending { + // If the vtxo is settled, add the record to the tx history. + if _, ok := settledVtxos[v.SpentBy]; ok { transactions = append(transactions, types.Transaction{ TransactionKey: types.TransactionKey{ RedeemTxid: v.Txid, }, Amount: v.Amount, Type: types.TxReceived, - IsPending: false, CreatedAt: getCreatedAtFromExpiry(roundLifetime, *v.ExpiresAt), + Settled: true, }) // Delete any duplicate in the indexed list. delete(indexedTxs, v.SpentBy) - // Ignore the spendable vtxo created by the claim. + // Ignore the spendable vtxo created by the settlement. ignoreVtxos[v.SpentBy] = struct{}{} continue } @@ -2522,7 +2422,8 @@ func vtxosToTxsCovenantless( indexedTxs[v.Txid] = tx } - // Add a transaction to the indexed list if not existing, it will be deleted if it's a duplicate. + // Add a transaction to the indexed list if not existing. + // This is an intermediate tx state that is updated in the next iterations. tx, ok := indexedTxs[v.SpentBy] if !ok { indexedTxs[v.SpentBy] = types.Transaction{ @@ -2532,7 +2433,6 @@ func vtxosToTxsCovenantless( }, Amount: v.Amount, Type: types.TxSent, - IsPending: false, CreatedAt: getCreatedAtFromExpiry(roundLifetime, *v.ExpiresAt), } continue @@ -2544,6 +2444,7 @@ func vtxosToTxsCovenantless( } for _, v := range spendable { + // Ignore the vtxo eventually. _, ok1 := ignoreVtxos[v.Txid] _, ok2 := ignoreVtxos[v.RoundTxid] if ok1 || ok2 { @@ -2555,6 +2456,7 @@ func vtxosToTxsCovenantless( } tx, ok := indexedTxs[txid] + // If there is no track of records, add a received tx record in the history. if !ok { redeemTxid := "" if v.RoundTxid == "" { @@ -2567,12 +2469,12 @@ func vtxosToTxsCovenantless( }, Amount: v.Amount, Type: types.TxReceived, - IsPending: v.Pending, CreatedAt: getCreatedAtFromExpiry(roundLifetime, *v.ExpiresAt), }) continue } + // Otherwise subtract the amount to find the actual spent amount. tx.Amount -= v.Amount if v.RedeemTx == "" { tx.RedeemTxid = "" diff --git a/pkg/client-sdk/example/covenant/alice_to_bob.go b/pkg/client-sdk/example/covenant/alice_to_bob.go index 99e628f..b11e6b4 100644 --- a/pkg/client-sdk/example/covenant/alice_to_bob.go +++ b/pkg/client-sdk/example/covenant/alice_to_bob.go @@ -55,8 +55,8 @@ func main() { onboardAmount := uint64(1_0000_0000) // 1 BTC log.Infof("alice is onboarding with %d sats offchain...", onboardAmount) - log.Infof("alice claiming onboarding funds...") - txid, err := aliceArkClient.Claim(ctx) + log.Infof("alice settled the onboard funds...") + txid, err := aliceArkClient.Settle(ctx) if err != nil { log.Fatal(err) } diff --git a/pkg/client-sdk/example/covenantless/alice_to_bob.go b/pkg/client-sdk/example/covenantless/alice_to_bob.go index 2563c11..04d2c65 100644 --- a/pkg/client-sdk/example/covenantless/alice_to_bob.go +++ b/pkg/client-sdk/example/covenantless/alice_to_bob.go @@ -84,13 +84,13 @@ func main() { log.Infof("alice onchain balance: %d", aliceBalance.OnchainBalance.SpendableAmount) log.Infof("alice offchain balance: %d", aliceBalance.OffchainBalance.Total) - log.Infof("alice claiming onboarding funds...") - txid, err := aliceArkClient.Claim(ctx) + log.Infof("alice is settling the onboard funds...") + txid, err := aliceArkClient.Settle(ctx) if err != nil { log.Fatal(err) } - log.Infof("alice claimed onboarding funds in round %s", txid) + log.Infof("alice settled the onboard funds in round %s", txid) fmt.Println("") log.Info("bob is setting up his ark wallet...") @@ -128,7 +128,7 @@ func main() { fmt.Println("") log.Infof("alice is sending %d sats to bob offchain...", amount) - if _, err = aliceArkClient.SendAsync(ctx, false, receivers); err != nil { + if _, err = aliceArkClient.SendOffChain(ctx, false, receivers); err != nil { log.Fatal(err) } @@ -158,13 +158,13 @@ func main() { log.Infof("bob offchain balance: %d", bobBalance.OffchainBalance.Total) fmt.Println("") - log.Info("bob is claiming the incoming payment...") - roundTxid, err := bobArkClient.Claim(ctx) + log.Info("bob is settling the received funds...") + roundTxid, err := bobArkClient.Settle(ctx) if err != nil { log.Fatal(err) } - log.Infof("bob claimed the incoming payment in round %s", roundTxid) + log.Infof("bob settled the received funds in round %s", roundTxid) time.Sleep(500 * time.Second) } diff --git a/pkg/client-sdk/example/covenantless/wasm/index.html b/pkg/client-sdk/example/covenantless/wasm/index.html index 65f71be..cd40119 100644 --- a/pkg/client-sdk/example/covenantless/wasm/index.html +++ b/pkg/client-sdk/example/covenantless/wasm/index.html @@ -90,19 +90,19 @@ } } - async function claimVtxos() { + async function settleVtxos() { const password = document.getElementById("c_password").value; if (!password) { - logMessage("Claim error: password is required"); + logMessage("Settle error: password is required"); return; } try { await unlock(password); - const txID = await claim(); - logMessage("Claimed money with tx ID: " + txID); + const txID = await settle(); + logMessage("Settled money with tx ID: " + txID); } catch (err) { - logMessage("Claim error: " + err.message); + logMessage("Settle error: " + err.message); } finally { await lock(password); } @@ -166,7 +166,7 @@
- +
diff --git a/pkg/client-sdk/explorer/explorer.go b/pkg/client-sdk/explorer/explorer.go index 949656f..3c64e63 100644 --- a/pkg/client-sdk/explorer/explorer.go +++ b/pkg/client-sdk/explorer/explorer.go @@ -13,6 +13,7 @@ import ( "github.com/ark-network/ark/common" "github.com/ark-network/ark/pkg/client-sdk/internal/utils" + "github.com/ark-network/ark/pkg/client-sdk/types" "github.com/btcsuite/btcd/btcutil/psbt" "github.com/btcsuite/btcd/wire" "github.com/vulpemventures/go-elements/psetv2" @@ -24,39 +25,6 @@ const ( LiquidExplorer = "liquid" ) -type Utxo struct { - Txid string - Vout uint32 - Amount uint64 - Asset string // liquid only - Delay uint - SpendableAt time.Time - CreatedAt time.Time -} - -func (u *Utxo) Sequence() (uint32, error) { - return common.BIP68Sequence(u.Delay) -} - -func newUtxo(explorerUtxo ExplorerUtxo, delay uint) Utxo { - utxoTime := explorerUtxo.Status.Blocktime - createdAt := time.Unix(utxoTime, 0) - if utxoTime == 0 { - createdAt = time.Time{} - utxoTime = time.Now().Unix() - } - - return Utxo{ - Txid: explorerUtxo.Txid, - Vout: explorerUtxo.Vout, - Amount: explorerUtxo.Amount, - Asset: explorerUtxo.Asset, - Delay: delay, - SpendableAt: time.Unix(utxoTime, 0).Add(time.Duration(delay) * time.Second), - CreatedAt: createdAt, - } -} - type ExplorerTx struct { Txid string `json:"txid"` Vout []struct { @@ -85,8 +53,8 @@ type SpentStatus struct { SpentBy string `json:"txid,omitempty"` } -func (e ExplorerUtxo) ToUtxo(delay uint) Utxo { - return newUtxo(e, delay) +func (e ExplorerUtxo) ToUtxo(delay uint, descriptor string) types.Utxo { + return newUtxo(e, delay, descriptor) } type Explorer interface { @@ -446,3 +414,23 @@ func parseBitcoinTx(txStr string) (string, string, error) { return txhex, txid, nil } + +func newUtxo(explorerUtxo ExplorerUtxo, delay uint, descriptor string) types.Utxo { + utxoTime := explorerUtxo.Status.Blocktime + createdAt := time.Unix(utxoTime, 0) + if utxoTime == 0 { + createdAt = time.Time{} + utxoTime = time.Now().Unix() + } + + return types.Utxo{ + Txid: explorerUtxo.Txid, + VOut: explorerUtxo.Vout, + Amount: explorerUtxo.Amount, + Asset: explorerUtxo.Asset, + Delay: delay, + SpendableAt: time.Unix(utxoTime, 0).Add(time.Duration(delay) * time.Second), + CreatedAt: createdAt, + Descriptor: descriptor, + } +} diff --git a/pkg/client-sdk/internal/utils/utils.go b/pkg/client-sdk/internal/utils/utils.go index 4e4f50e..256ca86 100644 --- a/pkg/client-sdk/internal/utils/utils.go +++ b/pkg/client-sdk/internal/utils/utils.go @@ -12,6 +12,7 @@ import ( "github.com/ark-network/ark/common" "github.com/ark-network/ark/pkg/client-sdk/client" + "github.com/ark-network/ark/pkg/client-sdk/types" "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg" @@ -23,10 +24,14 @@ import ( ) func CoinSelect( - vtxos []client.DescriptorVtxo, amount, dust uint64, sortByExpirationTime bool, -) ([]client.DescriptorVtxo, uint64, error) { - selected := make([]client.DescriptorVtxo, 0) - notSelected := make([]client.DescriptorVtxo, 0) + boardingUtxos []types.Utxo, + vtxos []client.DescriptorVtxo, + amount, + dust uint64, + sortByExpirationTime bool, +) ([]types.Utxo, []client.DescriptorVtxo, uint64, error) { + selected, notSelected := make([]client.DescriptorVtxo, 0), make([]client.DescriptorVtxo, 0) + selectedBoarding, notSelectedBoarding := make([]types.Utxo, 0), make([]types.Utxo, 0) selectedAmount := uint64(0) if sortByExpirationTime { @@ -38,6 +43,20 @@ func CoinSelect( return vtxos[i].ExpiresAt.Before(*vtxos[j].ExpiresAt) }) + + sort.SliceStable(boardingUtxos, func(i, j int) bool { + return boardingUtxos[i].SpendableAt.Before(boardingUtxos[j].SpendableAt) + }) + } + + for _, boardingUtxo := range boardingUtxos { + if selectedAmount >= amount { + notSelectedBoarding = append(notSelectedBoarding, boardingUtxo) + break + } + + selectedBoarding = append(selectedBoarding, boardingUtxo) + selectedAmount += boardingUtxo.Amount } for _, vtxo := range vtxos { @@ -51,7 +70,7 @@ func CoinSelect( } if selectedAmount < amount { - return nil, 0, fmt.Errorf("not enough funds to cover amount %d", amount) + return nil, nil, 0, fmt.Errorf("not enough funds to cover amount %d", amount) } change := selectedAmount - amount @@ -60,10 +79,13 @@ func CoinSelect( if len(notSelected) > 0 { selected = append(selected, notSelected[0]) change += notSelected[0].Amount + } else if len(notSelectedBoarding) > 0 { + selectedBoarding = append(selectedBoarding, notSelectedBoarding[0]) + change += notSelectedBoarding[0].Amount } } - return selected, change, nil + return selectedBoarding, selected, change, nil } func ParseLiquidAddress(addr string) ( diff --git a/pkg/client-sdk/types.go b/pkg/client-sdk/types.go index 5ddc4cf..7342c9b 100644 --- a/pkg/client-sdk/types.go +++ b/pkg/client-sdk/types.go @@ -3,6 +3,7 @@ package arksdk import ( "fmt" + "github.com/ark-network/ark/pkg/client-sdk/client" grpcclient "github.com/ark-network/ark/pkg/client-sdk/client/grpc" restclient "github.com/ark-network/ark/pkg/client-sdk/client/rest" "github.com/ark-network/ark/pkg/client-sdk/internal/utils" @@ -123,3 +124,10 @@ type balanceRes struct { offchainBalanceByExpiration map[int64]uint64 err error } + +type CoinSelectOptions struct { + // If true, coin selector will select coins closest to expiry first. + WithExpirySorting bool + // If specified, coin selector will select only coins in the list. + OutpointsFilter []client.Outpoint +} diff --git a/pkg/client-sdk/types/types.go b/pkg/client-sdk/types/types.go index 8bb005e..cb7b14e 100644 --- a/pkg/client-sdk/types/types.go +++ b/pkg/client-sdk/types/types.go @@ -73,7 +73,7 @@ type Transaction struct { TransactionKey Amount uint64 Type TxType - IsPending bool + Settled bool CreatedAt time.Time } @@ -103,3 +103,19 @@ type TransactionEvent struct { Tx Transaction Event EventType } + +type Utxo struct { + Txid string + VOut uint32 + Amount uint64 + Asset string // liquid only + Delay uint + SpendableAt time.Time + CreatedAt time.Time + Descriptor string + Spent bool +} + +func (u *Utxo) Sequence() (uint32, error) { + return common.BIP68Sequence(u.Delay) +} diff --git a/pkg/client-sdk/wasm/browser/exports.go b/pkg/client-sdk/wasm/browser/exports.go index 25ab37d..c601ddb 100644 --- a/pkg/client-sdk/wasm/browser/exports.go +++ b/pkg/client-sdk/wasm/browser/exports.go @@ -31,7 +31,7 @@ func init() { js.Global().Set("sendOnChain", SendOnChainWrapper()) js.Global().Set("sendOffChain", SendOffChainWrapper()) js.Global().Set("sendAsync", SendAsyncWrapper()) - js.Global().Set("claim", ClaimWrapper()) + js.Global().Set("settle", SettleWrapper()) js.Global().Set("unilateralRedeem", UnilateralRedeemWrapper()) js.Global().Set("collaborativeRedeem", CollaborativeRedeemWrapper()) js.Global().Set("getTransactionHistory", GetTransactionHistoryWrapper()) diff --git a/pkg/client-sdk/wasm/browser/wrappers.go b/pkg/client-sdk/wasm/browser/wrappers.go index 822baaa..8062e9b 100644 --- a/pkg/client-sdk/wasm/browser/wrappers.go +++ b/pkg/client-sdk/wasm/browser/wrappers.go @@ -14,6 +14,7 @@ import ( "time" arksdk "github.com/ark-network/ark/pkg/client-sdk" + "github.com/ark-network/ark/pkg/client-sdk/client" "github.com/ark-network/ark/pkg/client-sdk/wallet" singlekeywallet "github.com/ark-network/ark/pkg/client-sdk/wallet/singlekey" ) @@ -186,17 +187,17 @@ func SendOnChainWrapper() js.Func { if len(args) != 1 { return nil, errors.New("invalid number of args") } - receivers := make([]arksdk.Receiver, args[0].Length()) - for i := 0; i < args[0].Length(); i++ { - receiver := args[0].Index(i) - receivers[i] = arksdk.NewBitcoinReceiver( - receiver.Get("To").String(), uint64(receiver.Get("Amount").Int()), - ) + + receivers, err := parseReceivers(args[0]) + if err != nil { + return nil, err } - txID, err := arkSdkClient.SendOnChain( - context.Background(), receivers, - ) + if receivers == nil || len(receivers) == 0 { + return nil, errors.New("no receivers specified") + } + + txID, err := arkSdkClient.SendOnChain(context.Background(), receivers) if err != nil { return nil, err } @@ -209,13 +210,11 @@ func SendOffChainWrapper() js.Func { if len(args) != 2 { return nil, errors.New("invalid number of args") } + withExpiryCoinselect := args[0].Bool() - receivers := make([]arksdk.Receiver, args[1].Length()) - for i := 0; i < args[1].Length(); i++ { - receiver := args[1].Index(i) - receivers[i] = arksdk.NewBitcoinReceiver( - receiver.Get("To").String(), uint64(receiver.Get("Amount").Int()), - ) + receivers, err := parseReceivers(args[0]) + if err != nil { + return nil, err } txID, err := arkSdkClient.SendOffChain( @@ -233,13 +232,15 @@ func SendAsyncWrapper() js.Func { if len(args) != 2 { return nil, errors.New("invalid number of args") } + withExpiryCoinselect := args[0].Bool() - receivers := make([]arksdk.Receiver, args[1].Length()) - for i := 0; i < args[1].Length(); i++ { - receiver := args[1].Index(i) - receivers[i] = arksdk.NewBitcoinReceiver( - receiver.Get("To").String(), uint64(receiver.Get("Amount").Int()), - ) + receivers, err := parseReceivers(args[0]) + if err != nil { + return nil, err + } + + if receivers == nil || len(receivers) == 0 { + return nil, errors.New("no receivers specified") } txID, err := arkSdkClient.SendAsync( @@ -252,13 +253,13 @@ func SendAsyncWrapper() js.Func { }) } -func ClaimWrapper() js.Func { +func SettleWrapper() js.Func { return JSPromise(func(args []js.Value) (interface{}, error) { if len(args) != 0 { return nil, errors.New("invalid number of args") } - resp, err := arkSdkClient.Claim(context.Background()) + resp, err := arkSdkClient.Settle(context.Background()) if err != nil { return nil, err } @@ -306,7 +307,7 @@ func GetTransactionHistoryWrapper() js.Func { "redeemTxid": record.RedeemTxid, "amount": strconv.Itoa(int(record.Amount)), "type": record.Type, - "isPending": record.IsPending, + "settled": record.Settled, "createdAt": record.CreatedAt.Format(time.RFC3339), }) } @@ -433,3 +434,63 @@ func JSPromise(fn promise) js.Func { return promiseConstructor.New(handler) }) } + +func parseReceivers(jsReceivers js.Value) ([]arksdk.Receiver, error) { + if jsReceivers.IsNull() || jsReceivers.IsUndefined() { + return nil, nil // Return nil slice if input is null or undefined + } + + if jsReceivers.Type() != js.TypeObject || jsReceivers.Get("length").Type() != js.TypeNumber { + return nil, errors.New("invalid receivers argument: expected array") + } + + length := jsReceivers.Length() + if length == 0 { + return []arksdk.Receiver{}, nil // Return empty slice if input array is empty + } + + receivers := make([]arksdk.Receiver, length) + for i := 0; i < length; i++ { + receiver := jsReceivers.Index(i) + if receiver.Type() != js.TypeObject { + return nil, fmt.Errorf("invalid receiver at index %d: expected object", i) + } + + to := receiver.Get("To") + amount := receiver.Get("Amount") + if to.Type() != js.TypeString || amount.Type() != js.TypeNumber { + return nil, fmt.Errorf("invalid receiver at index %d: expected 'To' (string) and 'Amount' (number)", i) + } + + receivers[i] = arksdk.NewBitcoinReceiver(to.String(), uint64(amount.Int())) + } + + return receivers, nil +} + +func parseOutpoints(jsOutpoints js.Value) ([]client.Outpoint, error) { + if jsOutpoints.Length() == 0 { + return nil, nil + } + + outpoints := make([]client.Outpoint, jsOutpoints.Length()) + for i := 0; i < jsOutpoints.Length(); i++ { + jsOutpoint := jsOutpoints.Index(i) + if jsOutpoint.Type() != js.TypeObject { + return nil, fmt.Errorf("invalid outpoint at index %d: expected object", i) + } + + txid := jsOutpoint.Get("Txid") + vout := jsOutpoint.Get("Vout") + if txid.Type() != js.TypeString || vout.Type() != js.TypeNumber { + return nil, fmt.Errorf("invalid outpoint at index %d: expected 'Txid' (string) and 'Vout' (number)", i) + } + + outpoints[i] = client.Outpoint{ + Txid: txid.String(), + VOut: uint32(vout.Int()), + } + } + + return outpoints, nil +} diff --git a/pkg/client-sdk/wasm/build.go b/pkg/client-sdk/wasm/build.go index 1293aa4..4a5f02a 100644 --- a/pkg/client-sdk/wasm/build.go +++ b/pkg/client-sdk/wasm/build.go @@ -39,5 +39,3 @@ func PrintBuildInfo() { func GetVersion() string { return Version } - -// You can add more build-related functions here as needed diff --git a/pkg/client-sdk/wasm/main.go b/pkg/client-sdk/wasm/main.go index 9de1991..deee01c 100644 --- a/pkg/client-sdk/wasm/main.go +++ b/pkg/client-sdk/wasm/main.go @@ -21,8 +21,3 @@ func main() { println("ARK SDK WebAssembly module initialized") <-c } - -func init() { - // You can add any additional initialization here if needed - // This runs before the main function -} diff --git a/server/Makefile b/server/Makefile index d79bd8d..608d7ff 100755 --- a/server/Makefile +++ b/server/Makefile @@ -23,8 +23,8 @@ help: ## intergrationtest: runs integration tests integrationtest: @echo "Running integration tests..." - @go test -v -count 1 -timeout 400s github.com/ark-network/ark/server/test/e2e/covenant - @go test -v -count 1 -timeout 400s github.com/ark-network/ark/server/test/e2e/covenantless + @go test -v -count 1 -timeout 500s github.com/ark-network/ark/server/test/e2e/covenant + @go test -v -count 1 -timeout 500s github.com/ark-network/ark/server/test/e2e/covenantless ## lint: lint codebase lint: diff --git a/server/internal/core/application/covenantless.go b/server/internal/core/application/covenantless.go index 4a8693b..2273b85 100644 --- a/server/internal/core/application/covenantless.go +++ b/server/internal/core/application/covenantless.go @@ -51,10 +51,7 @@ type covenantlessService struct { currentRoundLock sync.Mutex currentRound *domain.Round treeSigningSessions map[string]*musigSigningSession - asyncPaymentsCache map[string]struct { // redeem txid -> receivers - receivers []domain.Receiver - expireAt int64 - } + asyncPaymentsCache map[string]asyncPaymentData } func NewCovenantlessService( @@ -69,11 +66,6 @@ func NewCovenantlessService( return nil, fmt.Errorf("failed to fetch pubkey: %s", err) } - asyncPaymentsCache := make(map[string]struct { - receivers []domain.Receiver - expireAt int64 - }) - svc := &covenantlessService{ network: network, pubkey: pubkey, @@ -90,7 +82,7 @@ func NewCovenantlessService( eventsCh: make(chan domain.RoundEvent), transactionEventsCh: make(chan TransactionEvent), currentRoundLock: sync.Mutex{}, - asyncPaymentsCache: asyncPaymentsCache, + asyncPaymentsCache: make(map[string]asyncPaymentData), treeSigningSessions: make(map[string]*musigSigningSession), boardingExitDelay: boardingExitDelay, } @@ -269,19 +261,16 @@ func (s *covenantlessService) CompleteAsyncPayment( vtxoPubkey := hex.EncodeToString(schnorr.SerializePubKey(vtxoTapKey)) - // all pending except the last one - isPending := outIndex < len(asyncPayData.receivers)-1 - vtxos = append(vtxos, domain.Vtxo{ VtxoKey: domain.VtxoKey{ Txid: redeemTxid, VOut: uint32(outIndex), }, - Pubkey: vtxoPubkey, - Amount: uint64(out.Value), - ExpireAt: asyncPayData.expireAt, - RedeemTx: redeemTx, - Pending: isPending, + Pubkey: vtxoPubkey, + Amount: uint64(out.Value), + ExpireAt: asyncPayData.expireAt, + RoundTxid: asyncPayData.roundTxid, + RedeemTx: redeemTx, }) } @@ -338,6 +327,8 @@ func (s *covenantlessService) CreateAsyncPayment( vtxosInputs := make([]domain.Vtxo, 0, len(inputs)) expiration := vtxos[0].ExpireAt + roundTxid := vtxos[0].RoundTxid + for _, vtxo := range vtxos { if vtxo.Spent { return "", fmt.Errorf("all vtxos must be unspent") @@ -350,11 +341,9 @@ func (s *covenantlessService) CreateAsyncPayment( if vtxo.Swept { return "", fmt.Errorf("all vtxos must be swept") } - if vtxo.Pending { - return "", fmt.Errorf("all vtxos must be claimed") - } if vtxo.ExpireAt < expiration { + roundTxid = vtxo.RoundTxid expiration = vtxo.ExpireAt } @@ -373,12 +362,10 @@ func (s *covenantlessService) CreateAsyncPayment( return "", fmt.Errorf("failed to parse redeem tx: %s", err) } - s.asyncPaymentsCache[redeemPtx.UnsignedTx.TxID()] = struct { - receivers []domain.Receiver - expireAt int64 - }{ + s.asyncPaymentsCache[redeemPtx.UnsignedTx.TxID()] = asyncPaymentData{ receivers: receivers, expireAt: expiration, + roundTxid: roundTxid, } return redeemTx, nil @@ -1588,6 +1575,12 @@ func findForfeitTxBitcoin( return "", fmt.Errorf("forfeit tx not found") } +type asyncPaymentData struct { + receivers []domain.Receiver + expireAt int64 + roundTxid string +} + // musigSigningSession holds the state of ephemeral nonces and signatures in order to coordinate the signing of the tree type musigSigningSession struct { lock sync.Mutex diff --git a/server/internal/core/application/utils.go b/server/internal/core/application/utils.go index d45c5d8..bd026dc 100644 --- a/server/internal/core/application/utils.go +++ b/server/internal/core/application/utils.go @@ -166,6 +166,24 @@ func (m *paymentsMap) update(payment domain.Payment) error { return fmt.Errorf("payment %s not found", payment.Id) } + sumOfInputs := uint64(0) + for _, input := range payment.Inputs { + sumOfInputs += input.Amount + } + + for _, boardingInput := range p.boardingInputs { + sumOfInputs += boardingInput.Amount + } + + sumOfOutputs := uint64(0) + for _, receiver := range payment.Receivers { + sumOfOutputs += receiver.Amount + } + + if sumOfInputs != sumOfOutputs { + return fmt.Errorf("sum of inputs %d does not match sum of outputs %d", sumOfInputs, sumOfOutputs) + } + p.Payment = payment return nil diff --git a/server/internal/core/domain/payment.go b/server/internal/core/domain/payment.go index 1500692..3af5984 100644 --- a/server/internal/core/domain/payment.go +++ b/server/internal/core/domain/payment.go @@ -123,5 +123,4 @@ type Vtxo struct { Swept bool ExpireAt int64 RedeemTx string // empty if in-round vtxo - Pending bool } diff --git a/server/internal/infrastructure/db/sqlite/migration/20240703120550_init.up.sql b/server/internal/infrastructure/db/sqlite/migration/20240703120550_init.up.sql index 584d4a3..96d3dd5 100644 --- a/server/internal/infrastructure/db/sqlite/migration/20240703120550_init.up.sql +++ b/server/internal/infrastructure/db/sqlite/migration/20240703120550_init.up.sql @@ -54,7 +54,6 @@ CREATE TABLE IF NOT EXISTS vtxo ( expire_at INTEGER NOT NULL, payment_id TEXT, redeem_tx TEXT, - pending BOOLEAN NOT NULL, PRIMARY KEY (txid, vout), FOREIGN KEY (payment_id) REFERENCES payment(id) ); diff --git a/server/internal/infrastructure/db/sqlite/sqlc/queries/models.go b/server/internal/infrastructure/db/sqlite/sqlc/queries/models.go index 03ae8a4..9e9d2bb 100644 --- a/server/internal/infrastructure/db/sqlite/sqlc/queries/models.go +++ b/server/internal/infrastructure/db/sqlite/sqlc/queries/models.go @@ -33,7 +33,6 @@ type PaymentVtxoVw struct { ExpireAt sql.NullInt64 PaymentID sql.NullString RedeemTx sql.NullString - Pending sql.NullBool } type Receiver struct { @@ -100,5 +99,4 @@ type Vtxo struct { ExpireAt int64 PaymentID sql.NullString RedeemTx sql.NullString - Pending bool } diff --git a/server/internal/infrastructure/db/sqlite/sqlc/queries/query.sql.go b/server/internal/infrastructure/db/sqlite/sqlc/queries/query.sql.go index d8d4f16..84b99ed 100644 --- a/server/internal/infrastructure/db/sqlite/sqlc/queries/query.sql.go +++ b/server/internal/infrastructure/db/sqlite/sqlc/queries/query.sql.go @@ -54,7 +54,7 @@ func (q *Queries) MarkVtxoAsSwept(ctx context.Context, arg MarkVtxoAsSweptParams } const selectNotRedeemedVtxos = `-- name: SelectNotRedeemedVtxos :many -SELECT vtxo.txid, vtxo.vout, vtxo.pubkey, vtxo.amount, vtxo.pool_tx, vtxo.spent_by, vtxo.spent, vtxo.redeemed, vtxo.swept, vtxo.expire_at, vtxo.payment_id, vtxo.redeem_tx, vtxo.pending FROM vtxo +SELECT vtxo.txid, vtxo.vout, vtxo.pubkey, vtxo.amount, vtxo.pool_tx, vtxo.spent_by, vtxo.spent, vtxo.redeemed, vtxo.swept, vtxo.expire_at, vtxo.payment_id, vtxo.redeem_tx FROM vtxo WHERE redeemed = false ` @@ -84,7 +84,6 @@ func (q *Queries) SelectNotRedeemedVtxos(ctx context.Context) ([]SelectNotRedeem &i.Vtxo.ExpireAt, &i.Vtxo.PaymentID, &i.Vtxo.RedeemTx, - &i.Vtxo.Pending, ); err != nil { return nil, err } @@ -100,7 +99,7 @@ func (q *Queries) SelectNotRedeemedVtxos(ctx context.Context) ([]SelectNotRedeem } const selectNotRedeemedVtxosWithPubkey = `-- name: SelectNotRedeemedVtxosWithPubkey :many -SELECT vtxo.txid, vtxo.vout, vtxo.pubkey, vtxo.amount, vtxo.pool_tx, vtxo.spent_by, vtxo.spent, vtxo.redeemed, vtxo.swept, vtxo.expire_at, vtxo.payment_id, vtxo.redeem_tx, vtxo.pending FROM vtxo +SELECT vtxo.txid, vtxo.vout, vtxo.pubkey, vtxo.amount, vtxo.pool_tx, vtxo.spent_by, vtxo.spent, vtxo.redeemed, vtxo.swept, vtxo.expire_at, vtxo.payment_id, vtxo.redeem_tx FROM vtxo WHERE redeemed = false AND pubkey = ? ` @@ -130,7 +129,6 @@ func (q *Queries) SelectNotRedeemedVtxosWithPubkey(ctx context.Context, pubkey s &i.Vtxo.ExpireAt, &i.Vtxo.PaymentID, &i.Vtxo.RedeemTx, - &i.Vtxo.Pending, ); err != nil { return nil, err } @@ -209,7 +207,7 @@ SELECT round.id, round.starting_timestamp, round.ending_timestamp, round.ended, round_payment_vw.id, round_payment_vw.round_id, round_tx_vw.id, round_tx_vw.tx, round_tx_vw.round_id, round_tx_vw.type, round_tx_vw.position, round_tx_vw.txid, round_tx_vw.tree_level, round_tx_vw.parent_txid, round_tx_vw.is_leaf, payment_receiver_vw.payment_id, payment_receiver_vw.pubkey, payment_receiver_vw.onchain_address, payment_receiver_vw.amount, - payment_vtxo_vw.txid, payment_vtxo_vw.vout, payment_vtxo_vw.pubkey, payment_vtxo_vw.amount, payment_vtxo_vw.pool_tx, payment_vtxo_vw.spent_by, payment_vtxo_vw.spent, payment_vtxo_vw.redeemed, payment_vtxo_vw.swept, payment_vtxo_vw.expire_at, payment_vtxo_vw.payment_id, payment_vtxo_vw.redeem_tx, payment_vtxo_vw.pending + payment_vtxo_vw.txid, payment_vtxo_vw.vout, payment_vtxo_vw.pubkey, payment_vtxo_vw.amount, payment_vtxo_vw.pool_tx, payment_vtxo_vw.spent_by, payment_vtxo_vw.spent, payment_vtxo_vw.redeemed, payment_vtxo_vw.swept, payment_vtxo_vw.expire_at, payment_vtxo_vw.payment_id, payment_vtxo_vw.redeem_tx FROM round LEFT OUTER JOIN round_payment_vw ON round.id=round_payment_vw.round_id LEFT OUTER JOIN round_tx_vw ON round.id=round_tx_vw.round_id @@ -275,7 +273,6 @@ func (q *Queries) SelectRoundWithRoundId(ctx context.Context, id string) ([]Sele &i.PaymentVtxoVw.ExpireAt, &i.PaymentVtxoVw.PaymentID, &i.PaymentVtxoVw.RedeemTx, - &i.PaymentVtxoVw.Pending, ); err != nil { return nil, err } @@ -295,7 +292,7 @@ SELECT round.id, round.starting_timestamp, round.ending_timestamp, round.ended, round_payment_vw.id, round_payment_vw.round_id, round_tx_vw.id, round_tx_vw.tx, round_tx_vw.round_id, round_tx_vw.type, round_tx_vw.position, round_tx_vw.txid, round_tx_vw.tree_level, round_tx_vw.parent_txid, round_tx_vw.is_leaf, payment_receiver_vw.payment_id, payment_receiver_vw.pubkey, payment_receiver_vw.onchain_address, payment_receiver_vw.amount, - payment_vtxo_vw.txid, payment_vtxo_vw.vout, payment_vtxo_vw.pubkey, payment_vtxo_vw.amount, payment_vtxo_vw.pool_tx, payment_vtxo_vw.spent_by, payment_vtxo_vw.spent, payment_vtxo_vw.redeemed, payment_vtxo_vw.swept, payment_vtxo_vw.expire_at, payment_vtxo_vw.payment_id, payment_vtxo_vw.redeem_tx, payment_vtxo_vw.pending + payment_vtxo_vw.txid, payment_vtxo_vw.vout, payment_vtxo_vw.pubkey, payment_vtxo_vw.amount, payment_vtxo_vw.pool_tx, payment_vtxo_vw.spent_by, payment_vtxo_vw.spent, payment_vtxo_vw.redeemed, payment_vtxo_vw.swept, payment_vtxo_vw.expire_at, payment_vtxo_vw.payment_id, payment_vtxo_vw.redeem_tx FROM round LEFT OUTER JOIN round_payment_vw ON round.id=round_payment_vw.round_id LEFT OUTER JOIN round_tx_vw ON round.id=round_tx_vw.round_id @@ -361,7 +358,6 @@ func (q *Queries) SelectRoundWithRoundTxId(ctx context.Context, txid string) ([] &i.PaymentVtxoVw.ExpireAt, &i.PaymentVtxoVw.PaymentID, &i.PaymentVtxoVw.RedeemTx, - &i.PaymentVtxoVw.Pending, ); err != nil { return nil, err } @@ -381,7 +377,7 @@ SELECT round.id, round.starting_timestamp, round.ending_timestamp, round.ended, round_payment_vw.id, round_payment_vw.round_id, round_tx_vw.id, round_tx_vw.tx, round_tx_vw.round_id, round_tx_vw.type, round_tx_vw.position, round_tx_vw.txid, round_tx_vw.tree_level, round_tx_vw.parent_txid, round_tx_vw.is_leaf, payment_receiver_vw.payment_id, payment_receiver_vw.pubkey, payment_receiver_vw.onchain_address, payment_receiver_vw.amount, - payment_vtxo_vw.txid, payment_vtxo_vw.vout, payment_vtxo_vw.pubkey, payment_vtxo_vw.amount, payment_vtxo_vw.pool_tx, payment_vtxo_vw.spent_by, payment_vtxo_vw.spent, payment_vtxo_vw.redeemed, payment_vtxo_vw.swept, payment_vtxo_vw.expire_at, payment_vtxo_vw.payment_id, payment_vtxo_vw.redeem_tx, payment_vtxo_vw.pending + payment_vtxo_vw.txid, payment_vtxo_vw.vout, payment_vtxo_vw.pubkey, payment_vtxo_vw.amount, payment_vtxo_vw.pool_tx, payment_vtxo_vw.spent_by, payment_vtxo_vw.spent, payment_vtxo_vw.redeemed, payment_vtxo_vw.swept, payment_vtxo_vw.expire_at, payment_vtxo_vw.payment_id, payment_vtxo_vw.redeem_tx FROM round LEFT OUTER JOIN round_payment_vw ON round.id=round_payment_vw.round_id LEFT OUTER JOIN round_tx_vw ON round.id=round_tx_vw.round_id @@ -447,7 +443,6 @@ func (q *Queries) SelectSweepableRounds(ctx context.Context) ([]SelectSweepableR &i.PaymentVtxoVw.ExpireAt, &i.PaymentVtxoVw.PaymentID, &i.PaymentVtxoVw.RedeemTx, - &i.PaymentVtxoVw.Pending, ); err != nil { return nil, err } @@ -463,7 +458,7 @@ func (q *Queries) SelectSweepableRounds(ctx context.Context) ([]SelectSweepableR } const selectSweepableVtxos = `-- name: SelectSweepableVtxos :many -SELECT vtxo.txid, vtxo.vout, vtxo.pubkey, vtxo.amount, vtxo.pool_tx, vtxo.spent_by, vtxo.spent, vtxo.redeemed, vtxo.swept, vtxo.expire_at, vtxo.payment_id, vtxo.redeem_tx, vtxo.pending FROM vtxo +SELECT vtxo.txid, vtxo.vout, vtxo.pubkey, vtxo.amount, vtxo.pool_tx, vtxo.spent_by, vtxo.spent, vtxo.redeemed, vtxo.swept, vtxo.expire_at, vtxo.payment_id, vtxo.redeem_tx FROM vtxo WHERE redeemed = false AND swept = false ` @@ -493,7 +488,6 @@ func (q *Queries) SelectSweepableVtxos(ctx context.Context) ([]SelectSweepableVt &i.Vtxo.ExpireAt, &i.Vtxo.PaymentID, &i.Vtxo.RedeemTx, - &i.Vtxo.Pending, ); err != nil { return nil, err } @@ -513,7 +507,7 @@ SELECT round.id, round.starting_timestamp, round.ending_timestamp, round.ended, round_payment_vw.id, round_payment_vw.round_id, round_tx_vw.id, round_tx_vw.tx, round_tx_vw.round_id, round_tx_vw.type, round_tx_vw.position, round_tx_vw.txid, round_tx_vw.tree_level, round_tx_vw.parent_txid, round_tx_vw.is_leaf, payment_receiver_vw.payment_id, payment_receiver_vw.pubkey, payment_receiver_vw.onchain_address, payment_receiver_vw.amount, - payment_vtxo_vw.txid, payment_vtxo_vw.vout, payment_vtxo_vw.pubkey, payment_vtxo_vw.amount, payment_vtxo_vw.pool_tx, payment_vtxo_vw.spent_by, payment_vtxo_vw.spent, payment_vtxo_vw.redeemed, payment_vtxo_vw.swept, payment_vtxo_vw.expire_at, payment_vtxo_vw.payment_id, payment_vtxo_vw.redeem_tx, payment_vtxo_vw.pending + payment_vtxo_vw.txid, payment_vtxo_vw.vout, payment_vtxo_vw.pubkey, payment_vtxo_vw.amount, payment_vtxo_vw.pool_tx, payment_vtxo_vw.spent_by, payment_vtxo_vw.spent, payment_vtxo_vw.redeemed, payment_vtxo_vw.swept, payment_vtxo_vw.expire_at, payment_vtxo_vw.payment_id, payment_vtxo_vw.redeem_tx FROM round LEFT OUTER JOIN round_payment_vw ON round.id=round_payment_vw.round_id LEFT OUTER JOIN round_tx_vw ON round.id=round_tx_vw.round_id @@ -579,7 +573,6 @@ func (q *Queries) SelectSweptRounds(ctx context.Context) ([]SelectSweptRoundsRow &i.PaymentVtxoVw.ExpireAt, &i.PaymentVtxoVw.PaymentID, &i.PaymentVtxoVw.RedeemTx, - &i.PaymentVtxoVw.Pending, ); err != nil { return nil, err } @@ -595,7 +588,7 @@ func (q *Queries) SelectSweptRounds(ctx context.Context) ([]SelectSweptRoundsRow } const selectVtxoByOutpoint = `-- name: SelectVtxoByOutpoint :one -SELECT vtxo.txid, vtxo.vout, vtxo.pubkey, vtxo.amount, vtxo.pool_tx, vtxo.spent_by, vtxo.spent, vtxo.redeemed, vtxo.swept, vtxo.expire_at, vtxo.payment_id, vtxo.redeem_tx, vtxo.pending FROM vtxo +SELECT vtxo.txid, vtxo.vout, vtxo.pubkey, vtxo.amount, vtxo.pool_tx, vtxo.spent_by, vtxo.spent, vtxo.redeemed, vtxo.swept, vtxo.expire_at, vtxo.payment_id, vtxo.redeem_tx FROM vtxo WHERE txid = ? AND vout = ? ` @@ -624,13 +617,12 @@ func (q *Queries) SelectVtxoByOutpoint(ctx context.Context, arg SelectVtxoByOutp &i.Vtxo.ExpireAt, &i.Vtxo.PaymentID, &i.Vtxo.RedeemTx, - &i.Vtxo.Pending, ) return i, err } const selectVtxosByPoolTxid = `-- name: SelectVtxosByPoolTxid :many -SELECT vtxo.txid, vtxo.vout, vtxo.pubkey, vtxo.amount, vtxo.pool_tx, vtxo.spent_by, vtxo.spent, vtxo.redeemed, vtxo.swept, vtxo.expire_at, vtxo.payment_id, vtxo.redeem_tx, vtxo.pending FROM vtxo +SELECT vtxo.txid, vtxo.vout, vtxo.pubkey, vtxo.amount, vtxo.pool_tx, vtxo.spent_by, vtxo.spent, vtxo.redeemed, vtxo.swept, vtxo.expire_at, vtxo.payment_id, vtxo.redeem_tx FROM vtxo WHERE pool_tx = ? ` @@ -660,7 +652,6 @@ func (q *Queries) SelectVtxosByPoolTxid(ctx context.Context, poolTx string) ([]S &i.Vtxo.ExpireAt, &i.Vtxo.PaymentID, &i.Vtxo.RedeemTx, - &i.Vtxo.Pending, ); err != nil { return nil, err } @@ -847,8 +838,8 @@ func (q *Queries) UpsertTransaction(ctx context.Context, arg UpsertTransactionPa } const upsertVtxo = `-- name: UpsertVtxo :exec -INSERT INTO vtxo (txid, vout, pubkey, amount, pool_tx, spent_by, spent, redeemed, swept, expire_at, redeem_tx, pending) -VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(txid, vout) DO UPDATE SET +INSERT INTO vtxo (txid, vout, pubkey, amount, pool_tx, spent_by, spent, redeemed, swept, expire_at, redeem_tx) +VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(txid, vout) DO UPDATE SET pubkey = EXCLUDED.pubkey, amount = EXCLUDED.amount, pool_tx = EXCLUDED.pool_tx, @@ -857,8 +848,7 @@ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(txid, vout) DO UPDATE SE redeemed = EXCLUDED.redeemed, swept = EXCLUDED.swept, expire_at = EXCLUDED.expire_at, - redeem_tx = EXCLUDED.redeem_tx, - pending = EXCLUDED.pending + redeem_tx = EXCLUDED.redeem_tx ` type UpsertVtxoParams struct { @@ -873,7 +863,6 @@ type UpsertVtxoParams struct { Swept bool ExpireAt int64 RedeemTx sql.NullString - Pending bool } func (q *Queries) UpsertVtxo(ctx context.Context, arg UpsertVtxoParams) error { @@ -889,7 +878,6 @@ func (q *Queries) UpsertVtxo(ctx context.Context, arg UpsertVtxoParams) error { arg.Swept, arg.ExpireAt, arg.RedeemTx, - arg.Pending, ) return err } diff --git a/server/internal/infrastructure/db/sqlite/sqlc/query.sql b/server/internal/infrastructure/db/sqlite/sqlc/query.sql index 05c4c56..2f4e83e 100644 --- a/server/internal/infrastructure/db/sqlite/sqlc/query.sql +++ b/server/internal/infrastructure/db/sqlite/sqlc/query.sql @@ -112,8 +112,8 @@ SELECT id FROM round WHERE starting_timestamp > ? AND starting_timestamp < ?; SELECT id FROM round; -- name: UpsertVtxo :exec -INSERT INTO vtxo (txid, vout, pubkey, amount, pool_tx, spent_by, spent, redeemed, swept, expire_at, redeem_tx, pending) -VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(txid, vout) DO UPDATE SET +INSERT INTO vtxo (txid, vout, pubkey, amount, pool_tx, spent_by, spent, redeemed, swept, expire_at, redeem_tx) +VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(txid, vout) DO UPDATE SET pubkey = EXCLUDED.pubkey, amount = EXCLUDED.amount, pool_tx = EXCLUDED.pool_tx, @@ -122,8 +122,7 @@ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(txid, vout) DO UPDATE SE redeemed = EXCLUDED.redeemed, swept = EXCLUDED.swept, expire_at = EXCLUDED.expire_at, - redeem_tx = EXCLUDED.redeem_tx, - pending = EXCLUDED.pending; + redeem_tx = EXCLUDED.redeem_tx; -- name: SelectSweepableVtxos :many SELECT sqlc.embed(vtxo) FROM vtxo diff --git a/server/internal/infrastructure/db/sqlite/vtxo_repo.go b/server/internal/infrastructure/db/sqlite/vtxo_repo.go index e47306a..7284cf7 100644 --- a/server/internal/infrastructure/db/sqlite/vtxo_repo.go +++ b/server/internal/infrastructure/db/sqlite/vtxo_repo.go @@ -51,7 +51,6 @@ func (v *vxtoRepository) AddVtxos(ctx context.Context, vtxos []domain.Vtxo) erro Swept: vtxo.Swept, ExpireAt: vtxo.ExpireAt, RedeemTx: sql.NullString{String: vtxo.RedeemTx, Valid: true}, - Pending: vtxo.Pending, }, ); err != nil { return err @@ -259,7 +258,6 @@ func rowToVtxo(row queries.Vtxo) domain.Vtxo { Swept: row.Swept, ExpireAt: row.ExpireAt, RedeemTx: row.RedeemTx.String, - Pending: row.Pending, } } diff --git a/server/internal/infrastructure/wallet/btc-embedded/wallet.go b/server/internal/infrastructure/wallet/btc-embedded/wallet.go index aec6d34..111b583 100644 --- a/server/internal/infrastructure/wallet/btc-embedded/wallet.go +++ b/server/internal/infrastructure/wallet/btc-embedded/wallet.go @@ -782,9 +782,9 @@ func (s *service) Status(ctx context.Context) (ports.WalletStatus, error) { w := s.wallet.InternalWallet() return status{ - true, - !w.Manager.IsLocked(), - w.ChainSynced(), + initialized: true, + unlocked: !w.Manager.IsLocked(), + synced: s.isSynced, }, nil } diff --git a/server/internal/interface/grpc/handlers/parser.go b/server/internal/interface/grpc/handlers/parser.go index b2c9981..c134474 100644 --- a/server/internal/interface/grpc/handlers/parser.go +++ b/server/internal/interface/grpc/handlers/parser.go @@ -124,7 +124,7 @@ func (v vtxoList) toProto() []*arkv1.Vtxo { SpentBy: vv.SpentBy, Swept: vv.Swept, RedeemTx: vv.RedeemTx, - Pending: vv.Pending, + IsOor: len(vv.RedeemTx) > 0, Pubkey: vv.Pubkey, }) } diff --git a/server/test/e2e/covenant/e2e_test.go b/server/test/e2e/covenant/e2e_test.go index 2fce392..651ef24 100644 --- a/server/test/e2e/covenant/e2e_test.go +++ b/server/test/e2e/covenant/e2e_test.go @@ -74,7 +74,7 @@ func TestSendOffchain(t *testing.T) { time.Sleep(5 * time.Second) - _, err = runArkCommand("claim", "--password", utils.Password) + _, err = runArkCommand("settle", "--password", utils.Password) require.NoError(t, err) time.Sleep(3 * time.Second) @@ -101,7 +101,7 @@ func TestUnilateralExit(t *testing.T) { time.Sleep(5 * time.Second) - _, err = runArkCommand("claim", "--password", utils.Password) + _, err = runArkCommand("settle", "--password", utils.Password) require.NoError(t, err) time.Sleep(3 * time.Second) @@ -142,7 +142,7 @@ func TestCollaborativeExit(t *testing.T) { time.Sleep(5 * time.Second) - _, err = runArkCommand("claim", "--password", utils.Password) + _, err = runArkCommand("settle", "--password", utils.Password) require.NoError(t, err) time.Sleep(3 * time.Second) @@ -164,7 +164,7 @@ func TestReactToSpentVtxosRedemption(t *testing.T) { time.Sleep(5 * time.Second) - _, err = client.Claim(ctx) + _, err = client.Settle(ctx) require.NoError(t, err) time.Sleep(3 * time.Second) @@ -216,7 +216,7 @@ func TestSweep(t *testing.T) { time.Sleep(5 * time.Second) - _, err = runArkCommand("claim", "--password", utils.Password) + _, err = runArkCommand("settle", "--password", utils.Password) require.NoError(t, err) time.Sleep(3 * time.Second) diff --git a/server/test/e2e/covenantless/e2e_test.go b/server/test/e2e/covenantless/e2e_test.go index d696319..685c376 100644 --- a/server/test/e2e/covenantless/e2e_test.go +++ b/server/test/e2e/covenantless/e2e_test.go @@ -77,7 +77,7 @@ func TestSendOffchain(t *testing.T) { time.Sleep(5 * time.Second) - _, err = runClarkCommand("claim", "--password", utils.Password) + _, err = runClarkCommand("settle", "--password", utils.Password) require.NoError(t, err) time.Sleep(3 * time.Second) @@ -91,7 +91,7 @@ func TestSendOffchain(t *testing.T) { require.NoError(t, json.Unmarshal([]byte(balanceStr), &balance)) require.NotZero(t, balance.Offchain.Total) - _, err = runClarkCommand("claim", "--password", utils.Password) + _, err = runClarkCommand("settle", "--password", utils.Password) require.NoError(t, err) balanceStr, err = runClarkCommand("balance") @@ -113,7 +113,7 @@ func TestUnilateralExit(t *testing.T) { time.Sleep(5 * time.Second) - _, err = runClarkCommand("claim", "--password", utils.Password) + _, err = runClarkCommand("settle", "--password", utils.Password) require.NoError(t, err) time.Sleep(3 * time.Second) @@ -155,11 +155,6 @@ func TestCollaborativeExit(t *testing.T) { time.Sleep(5 * time.Second) - _, err = runClarkCommand("claim", "--password", utils.Password) - require.NoError(t, err) - - time.Sleep(3 * time.Second) - _, err = runClarkCommand("redeem", "--amount", "1000", "--address", redeemAddress, "--password", utils.Password) require.NoError(t, err) } @@ -177,7 +172,7 @@ func TestReactToSpentVtxosRedemption(t *testing.T) { time.Sleep(5 * time.Second) - _, err = client.Claim(ctx) + _, err = client.Settle(ctx) require.NoError(t, err) _, err = client.SendOffChain(ctx, false, []arksdk.Receiver{arksdk.NewBitcoinReceiver(offchainAddress, 1000)}) @@ -217,71 +212,105 @@ func TestReactToSpentVtxosRedemption(t *testing.T) { } func TestReactToAsyncSpentVtxosRedemption(t *testing.T) { - t.Run("receiver claimed funds", func(t *testing.T) { - ctx := context.Background() - sdkClient, grpcClient := setupArkSDK(t) - defer grpcClient.Close() + ctx := context.Background() + sdkClient, grpcClient := setupArkSDK(t) + defer grpcClient.Close() - offchainAddress, boardingAddress, err := sdkClient.Receive(ctx) - require.NoError(t, err) + offchainAddress, boardingAddress, err := sdkClient.Receive(ctx) + require.NoError(t, err) - _, err = utils.RunCommand("nigiri", "faucet", boardingAddress) - require.NoError(t, err) + _, err = utils.RunCommand("nigiri", "faucet", boardingAddress) + require.NoError(t, err) - time.Sleep(5 * time.Second) + time.Sleep(5 * time.Second) - roundId, err := sdkClient.Claim(ctx) - require.NoError(t, err) + roundId, err := sdkClient.Settle(ctx) + require.NoError(t, err) - err = utils.GenerateBlock() - require.NoError(t, err) + err = utils.GenerateBlock() + require.NoError(t, err) - _, err = sdkClient.SendAsync(ctx, false, []arksdk.Receiver{arksdk.NewBitcoinReceiver(offchainAddress, 1000)}) - require.NoError(t, err) + _, err = sdkClient.SendOffChain(ctx, false, []arksdk.Receiver{arksdk.NewBitcoinReceiver(offchainAddress, 1000)}) + require.NoError(t, err) - _, err = sdkClient.Claim(ctx) - require.NoError(t, err) + _, err = sdkClient.Settle(ctx) + require.NoError(t, err) - time.Sleep(5 * time.Second) + time.Sleep(5 * time.Second) - _, spentVtxos, err := sdkClient.ListVtxos(ctx) - require.NoError(t, err) - require.NotEmpty(t, spentVtxos) + _, spentVtxos, err := sdkClient.ListVtxos(ctx) + require.NoError(t, err) + require.NotEmpty(t, spentVtxos) - var vtxo client.Vtxo + var vtxo client.Vtxo - for _, v := range spentVtxos { - if v.RoundTxid == roundId { - vtxo = v - break - } + for _, v := range spentVtxos { + if v.RoundTxid == roundId { + vtxo = v + break } - require.NotEmpty(t, vtxo) + } + require.NotEmpty(t, vtxo) - round, err := grpcClient.GetRound(ctx, vtxo.RoundTxid) + round, err := grpcClient.GetRound(ctx, vtxo.RoundTxid) + require.NoError(t, err) + + expl := explorer.NewExplorer("http://localhost:3000", common.BitcoinRegTest) + + branch, err := redemption.NewCovenantlessRedeemBranch(expl, round.Tree, vtxo) + require.NoError(t, err) + + txs, err := branch.RedeemPath() + require.NoError(t, err) + + for _, tx := range txs { + _, err := expl.Broadcast(tx) require.NoError(t, err) + } - expl := explorer.NewExplorer("http://localhost:3000", common.BitcoinRegTest) + // give time for the ASP to detect and process the fraud + time.Sleep(50 * time.Second) - branch, err := redemption.NewCovenantlessRedeemBranch(expl, round.Tree, vtxo) - require.NoError(t, err) + balance, err := sdkClient.Balance(ctx, false) + require.NoError(t, err) - txs, err := branch.RedeemPath() - require.NoError(t, err) + require.Empty(t, balance.OnchainBalance.LockedAmount) +} - for _, tx := range txs { - _, err := expl.Broadcast(tx) - require.NoError(t, err) - } +func TestChainAsyncPayments(t *testing.T) { + var receive utils.ArkReceive + receiveStr, err := runClarkCommand("receive") + require.NoError(t, err) - // give time for the ASP to detect and process the fraud - time.Sleep(50 * time.Second) + err = json.Unmarshal([]byte(receiveStr), &receive) + require.NoError(t, err) - balance, err := sdkClient.Balance(ctx, false) - require.NoError(t, err) + _, err = utils.RunCommand("nigiri", "faucet", receive.Boarding) + require.NoError(t, err) - require.Empty(t, balance.OnchainBalance.LockedAmount) - }) + time.Sleep(5 * time.Second) + + _, err = runClarkCommand("settle", "--password", utils.Password) + require.NoError(t, err) + + time.Sleep(3 * time.Second) + + _, err = runClarkCommand("send", "--amount", "10000", "--to", receive.Offchain, "--password", utils.Password) + require.NoError(t, err) + + var balance utils.ArkBalance + balanceStr, err := runClarkCommand("balance") + require.NoError(t, err) + require.NoError(t, json.Unmarshal([]byte(balanceStr), &balance)) + require.NotZero(t, balance.Offchain.Total) + + _, err = runClarkCommand("send", "--amount", "10000", "--to", receive.Offchain, "--password", utils.Password) + require.NoError(t, err) + + balanceStr, err = runClarkCommand("balance") + require.NoError(t, err) + require.NoError(t, json.Unmarshal([]byte(balanceStr), &balance)) + require.NotZero(t, balance.Offchain.Total) } func TestAliceSeveralPaymentsToBob(t *testing.T) { @@ -300,9 +329,6 @@ func TestAliceSeveralPaymentsToBob(t *testing.T) { time.Sleep(5 * time.Second) - _, err = alice.Claim(ctx) - require.NoError(t, err) - bobAddress, _, err := bob.Receive(ctx) require.NoError(t, err) @@ -315,12 +341,6 @@ func TestAliceSeveralPaymentsToBob(t *testing.T) { require.NoError(t, err) require.Len(t, bobVtxos, 1) - _, err = bob.Claim(ctx) - require.NoError(t, err) - - _, err = alice.Claim(ctx) - require.NoError(t, err) - _, err = alice.SendOffChain(ctx, false, []arksdk.Receiver{arksdk.NewBitcoinReceiver(bobAddress, 10000)}) require.NoError(t, err) @@ -339,7 +359,7 @@ func TestAliceSeveralPaymentsToBob(t *testing.T) { require.NoError(t, err) require.Len(t, bobVtxos, 3) - _, err = alice.SendAsync(ctx, false, []arksdk.Receiver{arksdk.NewBitcoinReceiver(bobAddress, 10000)}) + _, err = alice.SendOffChain(ctx, false, []arksdk.Receiver{arksdk.NewBitcoinReceiver(bobAddress, 10000)}) require.NoError(t, err) time.Sleep(2 * time.Second) @@ -348,9 +368,6 @@ func TestAliceSeveralPaymentsToBob(t *testing.T) { require.NoError(t, err) require.Len(t, bobVtxos, 4) - _, err = alice.Claim(ctx) - require.NoError(t, err) - // bobVtxos should be unique uniqueVtxos := make(map[string]struct{}) for _, v := range bobVtxos { @@ -358,9 +375,7 @@ func TestAliceSeveralPaymentsToBob(t *testing.T) { } require.Len(t, uniqueVtxos, 4) - _, err = bob.Claim(ctx) require.NoError(t, err) - } func TestSweep(t *testing.T) { @@ -376,7 +391,7 @@ func TestSweep(t *testing.T) { time.Sleep(5 * time.Second) - _, err = runClarkCommand("claim", "--password", utils.Password) + _, err = runClarkCommand("settle", "--password", utils.Password) require.NoError(t, err) time.Sleep(3 * time.Second)