Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add mint v1 support #53

Merged
merged 3 commits into from
Oct 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion examples/booking/mintnbuy.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,9 @@ func main() {
}

bt, err := bookingtoken.NewBookingtoken(common.HexToAddress("0xe55E387F5474a012D1b048155E25ea78C7DBfBBC"), client)
if err != nil {
sugar.Fatalf("Failed to create BookingToken contract binding: %v", err)
}

// token uri
tokenURI := "data:application/json;base64,eyJuYW1lIjoiYm90IGNtYWNjb3VudCBwa2cgYm9va2luZyB0b2tlbiB0ZXN0In0K"
Expand Down Expand Up @@ -193,7 +196,7 @@ func main() {

switch price.Currency.Currency.(type) {
case *typesv2.Currency_NativeToken:
bigIntPrice, err = bs.ConvertPriceToBigInt(price, int32(18)) // CAM uses 18 decimals
bigIntPrice, err = bs.ConvertPriceToBigInt(price.Value, price.Decimals, int32(18)) // CAM uses 18 decimals
if err != nil {
sugar.Errorf("Failed to convert price to big.Int: %v", err)
return
Expand Down
6 changes: 6 additions & 0 deletions examples/rpc/partner-plugin/handlers/mint_v1.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
typesv1 "buf.build/gen/go/chain4travel/camino-messenger-protocol/protocolbuffers/go/cmp/types/v1"
"github.com/chain4travel/camino-messenger-bot/internal/metadata"
"google.golang.org/grpc"
"google.golang.org/protobuf/types/known/emptypb"
"google.golang.org/protobuf/types/known/timestamppb"
)

Expand All @@ -37,6 +38,11 @@ func (*MintServiceV1Server) Mint(ctx context.Context, _ *bookv1.MintRequest) (*b
Price: &typesv1.Price{
Value: "1",
Decimals: 9,
Currency: &typesv1.Currency{
Currency: &typesv1.Currency_NativeToken{
NativeToken: &emptypb.Empty{},
},
},
},
}

Expand Down
241 changes: 241 additions & 0 deletions internal/messaging/mint.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
package messaging

import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"math/big"
"time"

notificationv1 "buf.build/gen/go/chain4travel/camino-messenger-protocol/protocolbuffers/go/cmp/services/notification/v1"
typesv1 "buf.build/gen/go/chain4travel/camino-messenger-protocol/protocolbuffers/go/cmp/types/v1"
"github.com/chain4travel/camino-messenger-contracts/go/contracts/bookingtoken"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
ethTypes "github.com/ethereum/go-ethereum/core/types"
"google.golang.org/grpc"
grpc_metadata "google.golang.org/grpc/metadata"
"google.golang.org/protobuf/types/known/timestamppb"
)

// Mints a BookingToken with the supplier private key and reserves it for the buyer address
// For testing you can use this uri: "data:application/json;base64,eyJuYW1lIjoiQ2FtaW5vIE1lc3NlbmdlciBCb29raW5nVG9rZW4gVGVzdCJ9Cg=="
func (h *evmResponseHandler) mint(
ctx context.Context,
reservedFor common.Address,
uri string,
expiration *big.Int,
price *big.Int,
paymentToken common.Address,
) (string, *big.Int, error) {
// TODO:
// (in booking package)
// define paymentToken from currency
// if TokenCurrency get paymentToken contract and call decimals()
// calculate the price in big int without loosing precision

tx, err := h.bookingService.MintBookingToken(
reservedFor,
uri,
expiration,
price,
paymentToken)
if err != nil {
return "", nil, err
}

// Wait for transaction to be mined
receipt, err := bind.WaitMined(ctx, h.ethClient, tx)
if err != nil {
return "", nil, err
}

tokenID := big.NewInt(0)

for _, mLog := range receipt.Logs {
event, err := h.bookingToken.ParseTokenReserved(*mLog)
if err == nil {
tokenID = event.TokenId
h.logger.Infof("[TokenReserved] TokenID: %s ReservedFor: %s Price: %s, PaymentToken: %s", event.TokenId, event.ReservedFor, event.Price, event.PaymentToken)
}
}

return tx.Hash().Hex(), tokenID, nil
}

// TODO @VjeraTurk code that creates and handles context should be improved, since its not doing job in separate goroutine,
// Buys a token with the buyer private key. Token must be reserved for the buyer address.
func (h *evmResponseHandler) buy(ctx context.Context, tokenID *big.Int) (string, error) {
tx, err := h.bookingService.BuyBookingToken(tokenID)
if err != nil {
return "", err
}

receipt, err := h.waitTransaction(ctx, tx)
if err != nil {
return "", err
}
if receipt.Status != ethTypes.ReceiptStatusSuccessful {
return "", fmt.Errorf("transaction failed: %v", receipt)
}

h.logger.Infof("Transaction sent!\nTransaction hash: %s\n", tx.Hash().Hex())

return tx.Hash().Hex(), nil
}

func (h *evmResponseHandler) onBookingTokenMint(tokenID *big.Int, mintID *typesv1.UUID, buyableUntil time.Time) {
notificationClient := h.serviceRegistry.NotificationClient()
expirationTimer := &time.Timer{}

unsubscribeTokenBought, err := h.evmEventListener.RegisterTokenBoughtHandler(
h.bookingTokenAddress,
[]*big.Int{tokenID},
nil,
func(e any) {
expirationTimer.Stop()
h.logger.Infof("Token bought event received for token %s", tokenID.String())
event := e.(*bookingtoken.BookingtokenTokenBought)

if _, err := notificationClient.TokenBoughtNotification(
context.Background(),
&notificationv1.TokenBought{
TokenId: tokenID.Uint64(),
TxId: event.Raw.TxHash.Hex(),
MintId: mintID,
},
grpc.Header(&grpc_metadata.MD{}),
); err != nil {
h.logger.Errorf("error calling partner plugin TokenBoughtNotification service: %v", err)
}
},
)
if err != nil {
h.logger.Errorf("failed to register handler: %v", err)
// TODO @evlekht send some notification to partner plugin
return
}

expirationTimer = time.AfterFunc(time.Until(buyableUntil), func() {
unsubscribeTokenBought()
h.logger.Infof("Token %s expired", tokenID.String())

if _, err := notificationClient.TokenExpiredNotification(
context.Background(),
&notificationv1.TokenExpired{
TokenId: tokenID.Uint64(),
MintId: mintID,
},
grpc.Header(&grpc_metadata.MD{}),
); err != nil {
h.logger.Errorf("error calling partner plugin TokenExpiredNotification service: %v", err)
}
})
}

// TODO @evlekht check if those structs are needed as exported here, otherwise make them private or move to another pkg
type hotelAtrribute struct {
TraitType string `json:"trait_type"`
Value string `json:"value"`
}

type hotelJSON struct {
Name string `json:"name"`
Description string `json:"description,omitempty"`
Date string `json:"date,omitempty"`
ExternalURL string `json:"external_url,omitempty"`
Image string `json:"image,omitempty"`
Attributes []hotelAtrribute `json:"attributes,omitempty"`
}

// Generates a token data URI from a MintResponse object. Returns jsonPlain and a
// data URI with base64 encoded json data.
//
// TODO: @havan: We need decide what data needs to be in the tokenURI JSON and add
// those fields to the MintResponse. These will be shown in the UI of wallets,
// explorers etc.
func createTokenURIforMintResponse(mintID, bookingReference string) (string, string, error) {
// TODO: What should we use for a token name? This will be shown in the UI of wallets, explorers etc.
name := "CM Booking Token"

// TODO: What should we use for a token description? This will be shown in the UI of wallets, explorers etc.
description := "This NFT represents the booking with the specified attributes."

// Dummy data
date := "2024-09-27"

externalURL := "https://camino.network"

// Placeholder Image
image := "https://camino.network/static/images/N9IkxmG-Sg-1800.webp"

attributes := []hotelAtrribute{
{
TraitType: "Mint ID",
Value: mintID,
},
{
TraitType: "Reference",
Value: bookingReference,
},
}

jsonPlain, jsonEncoded, err := generateAndEncodeJSON(
name,
description,
date,
externalURL,
image,
attributes,
)
if err != nil {
return "", "", err
}

// Add data URI scheme
tokenURI := "data:application/json;base64," + jsonEncoded

return jsonPlain, tokenURI, nil
}

func generateAndEncodeJSON(name, description, date, externalURL, image string, attributes []hotelAtrribute) (string, string, error) {
hotel := hotelJSON{
Name: name,
Description: description,
Date: date,
ExternalURL: externalURL,
Image: image,
Attributes: attributes,
}

jsonData, err := json.Marshal(hotel)
if err != nil {
return "", "", err
}

encoded := base64.StdEncoding.EncodeToString(jsonData)
return string(jsonData), encoded, nil
}

func verifyAndFixBuyableUntil(buyableUntil *timestamppb.Timestamp, currentTime time.Time) (*timestamppb.Timestamp, error) {
switch {
case buyableUntil == nil || buyableUntil.Seconds == 0:
// BuyableUntil not set
return timestamppb.New(currentTime.Add(buyableUntilDurationDefault)), nil

case buyableUntil.Seconds < timestamppb.New(currentTime).Seconds:
// BuyableUntil in the past
return nil, fmt.Errorf("refused to mint token - BuyableUntil in the past: %v", buyableUntil)

case buyableUntil.Seconds < timestamppb.New(currentTime.Add(buyableUntilDurationMinimal)).Seconds:
// BuyableUntil too early
return timestamppb.New(currentTime.Add(buyableUntilDurationMinimal)), nil

case buyableUntil.Seconds > timestamppb.New(currentTime.Add(buyableUntilDurationMaximal)).Seconds:
// BuyableUntil too late
return timestamppb.New(currentTime.Add(buyableUntilDurationMaximal)), nil
}

return buyableUntil, nil
}
Loading