Founder & builder in the Aptos ecosystem.
Founder & builder in the Aptos ecosystem.

Subscribe to Aptos Intros: An intro to building in Move/Aptos

Subscribe to Aptos Intros: An intro to building in Move/Aptos
Share Dialog
Share Dialog


<100 subscribers
<100 subscribers
Editor’s Note: I’ve been utilizing the Mirror blogging platform primarily because it signals that I’m a cool crypto guy. However, it’s a bit lacking in functionality. Several folks have expressed an interest in a mechanism to ask questions/post comments on each episode. As such, I’m going to move this blog to Substack (not paid) starting with the next episode. Until then, feel free to hit me up at Twitter or on the Aptos discord. I’ll continue crosspost here on Mirror because I am, in fact, very much a cool crypto guy.
Learning Objectives: Global Storage operators, External Modules, Vectors, Generics, Acquires
We’re picking up where we left off at the last episode - so if this is your first time here check that out first. As always, the repo for the code as it should be at the end of this episode is tagged “Episode-2”.
Last episode we learned how to create a ConcertTicket resource and enable a user to create the ticket and transfer it to their account with:
public fun create_ticket(recipient: &signer, seat: vector<u8>, ticket_code: vector<u8>) {
move_to<ConcertTicket>(recipient, ConcertTicket {seat, ticket_code})
}
While that’s interesting, I’d rather sell tickets to the concert. And we really don’t want the user to be able to assign their seat or everyone’s going to be in the front row. So, let’s jump right in.
We’re going to add several new error constant declarations, so let’s add them just after the structs as:
const ENO_VENUE: u64 = 0;
const ENO_TICKETS: u64 = 1;
const ENO_ENVELOPE: u64 = 2;
const EINVALID_TICKET_COUNT: u64 = 3;
const EINVALID_TICKET: u64 = 4;
const EINVALID_PRICE: u64 = 5;
const EMAX_SEATS: u64 = 6;
const EINVALID_BALANCE: u64 = 7;
The first thing we need to figure out how to do is to limit the actual tickets that can be bought/sold. In the physical world, that’s a fairly easy constraint to comprehend: a venue only has so many physical seats it can sell tickets for. You can’t sell two tickets to the same seat (unless you are United Airlines apparently). Let’s continue to model our resources after their physical world counterpart and create a new struct we’ll call Venue:
struct Venue has key {
available_tickets: vector<ConcertTicket>,
max_seats: u64
}
Add that code to the top of your module just past the ConcertTicket struct. Venue is altogether different from our ConcertTicket struct, with the new twist of a vector of another struct. A venue could be anything from a small club with a handful of seats to a 50,000 seat stadium. Let’s give a venue owner the ability to create a Venue in their Aptos account/wallet:
public fun init_venue(venue_owner: &signer, max_seats: u64) {
let available_tickets = Vector::empty<ConcertTicket>();
move_to<Venue>(venue_owner, Venue {available_tickets, max_seats})
}
This introduces two new concepts that we’ll cover individually: Std::Vector and Generics.
(If you don’t get that subtitle reference, we can’t be friends)
Vector is part of the standard library that comes from the Move language. While vector<T> is a native primitive type, the Vector module adds some wrappers that makes it easier for us to do things with resources. There is a pretty good intro here:
https://diem.github.io/move/vector.html
I overheard my 16 year-old daughter ask a friend of hers “what’s the tea?” while discussing some high school drama - to which I quickly replied, “A generic class or interface that is parameterized over types is typically represented as T in the interface definition.” Apparently, I was incorrect answering “what’s the T” in her context and my helpful answer was not appreciated.
Most of you will be familiar with the <T> notation, but if it’s the first time you’re seeing it, just think of <T> as a parameter for types. In our Move context, we call that a Generic. We sort of glossed over that topic in Episode 1 and there were a few questions.
Since Vector is part of a library, we have to tell the compiler we’re going to use it. In your code just under Std::Signer, let’s add:
use Std::Signer;
use Std::Vector;
In our init_venue function above, we are calling the function Std::Vector::empty<T>() which creates an empty vector<T>. If we peek into the Vector.move module at in the repo at aptos-core/aptos-move/framework/move-stdlib/sources/Vector.move, we can see that Vector::empty is just a function:
// Create an empty vector.
native public fun empty<Element>(): vector<Element>;
Wait, what happened to the <T>? Well, generics are so generic we don’t even specify what you have to call them. Whatever name you put inside the < > in essence becomes a variable that refers to a Type. When a function is defined as:
native public fun empty<Element>(): vector<Element>;
whatever Type we pass in the first element is represented in the rest of the function. So, dropping in <ConcertTicket> for <Element>, the actual function basically becomes:
native public fun empty<ConcertTicket>(): vector<ConcertTicket>;
and anywhere else <Element> appeared in the function would become <ConcertTicket>. That’s all handled behind the scenes for us and we don’t have to worry about it. When defining your own functions with generic type parameters, you can call the parameter T, or Element, or WhateverIDarnWellPlease. It’s just a variable name that holds a type.
So we’ve created a vector of ConcertTicket with
let available_tickets = Vector::empty<ConcertTicket>();
Then we create the Venue resource and move it into our account:
move_to<Venue>(venue_owner, Venue {available_tickets, max_seats})
Now, we could combine those two into one line of code with:
move_to<Venue>(venue_owner, Venue {available_tickets: Vector::empty<ConcertTicket>(), max_seats})
But I think it’s more readable splitting into two lines. Also, to make a very minor point, when setting the property value of a struct, I can use a variable named available_tickets and pass it to Venue {available_tickets} and it will work fine. Because the variable is named the same thing as the parameter, what the compiler sees is Venue {available_tickets: available_tickets}. If you’re passing in anything else, you’ve got to explicitly reference the property name like we did in the one line version above.
Let’s test this and make sure it’s functioning. To start, let’s delete all of our test code from last episode:
#[test(recipient = @0x1)]
public(script) fun sender_can_create_ticket(recipient: signer){
....
}
and replace it with this starting point:
#[test(venue_owner = @0x1)]
public(script) fun sender_can_buy_ticket(venue_owner: signer) {
let venue_owner_addr = Signer::address_of(&venue_owner);
// initialize the venue
init_venue(&venue_owner, 3);
assert!(exists<Venue>(venue_owner_addr), ENO_VENUE);
}
We’ve setup our test to run with an account venue_owner which we are assigning address 0x1 and then we call the init_venue function to create the venue with a maximum of 3 seats. We use our exists function again to make sure the venue was created. From the terminal, run cargo tests and you’ll pass all green.
We can call the init_venue function from our account and we now own an empty arena. Let’s create some tickets to sell. Since we have a maximum number of tickets we can create, it would be helpful to know how many tickets we have already created. Let’s add a helper function of:
public fun available_ticket_count(venue_owner_addr: address): u64 acquires Venue {
let venue = borrow_global<Venue>(venue_owner_addr);
Vector::length<ConcertTicket>(&venue.available_tickets) }
We’ve got several new things going on here. Let’s look at ‘acquires’ first. I’ve got three kids, aged 16, 17 and 21 (yes, I’m old). When each of them first started driving on their own, we had a rule that they always had to tell us where they were going. On occasion, I would use that ever so handy “Find My” iPhone app to see where they were (and they were aware of this). If they ever popped up in a location they hadn’t told me about, I’d give them a call with “I see you are in this location, but you didn’t tell me you were going there. Why did you not want me to know you were going to be there?” It was a temporary practice to ensure their new found freedom wasn’t enabling bad decision making.
In essence, that’s what ‘acquires’ does in the Move language. Move is built for safety. If you intend to access a Venue type from global storage, you’ve got to state that with acquires Venue - otherwise compiler dad is going to call you with The call acquires 'TicketTutorial::Tickets::Venue', but the 'acquires' list for the current function does not contain this type. It must be present in the calling context's acquires list and your code won’t build. Couldn’t the compiler just infer the ‘acquires’ based on my code? Well, sure, but that doesn’t accomplish the safety objective any more than inferring my son is supposed to be at the lake during school hours just because that’s where his phone shows up on the map. It’s a compiler check to make sure what we’re doing in code is what we intended. There is a detailed explanation here:
https://diem.github.io/move/global-storage-operators.html
Now that compiler dad knows where we’re going to be with our ‘acquires’, we can see how many tickets we’ve created. The borrow_global is a global storage operator that allows us to read a particular data type from an account’s global storage. The line borrow_global<Venue>(venue_owner_addr) lets us read the Venue struct belonging to the account owner. We don’t have to specify an index or any other reference because Move will only allow us to create one of any struct at an address. We can either have no Venues or we can have one. That’s it. The ‘borrow’ process ensures data integrity. We can’t borrow a resource if it’s already been borrowed somewhere else. This ensures that we are always getting accurate data. We’ll see this at play shortly.
Key Point: An account can only hold one instance of any resource type.
We’ve got our Venue resource sitting in the aptly named variable venue, now we want to see how many tickets are in the available_tickets property. The function Vector::length gives us that number by passing in the vector we want the count of. Since the vector we want to know about contains ConcertTickets, we pass in the struct as the type parameter and a reference to the available_tickets property of venue. You probably noticed there is no explicit return statement for our function. In the Move language, when the last line of a function returns a value (and we don’t end the line with a semicolon), that value is returned as the value of the function. In essence, the last line of:
Vector::length<ConcertTicket>(&venue.available_tickets)
is actually compiled as if it were:
return Vector::length<ConcertTicket>(&venue.available_tickets);
It’s a handy feature that saves us a tiny bit of time.
We’ve got our helper function now, so let’s create some tickets. We’ll modify our previous create_ticket function from Episode 1 to:
public fun create_ticket(venue_owner: &signer, seat: vector<u8>, ticket_code: vector<u8>, price: u64) acquires Venue {
let venue_owner_addr = Signer::address_of(venue_owner);
assert!(exists<Venue>(venue_owner_addr), ENO_VENUE);
let current_seat_count = available_ticket_count(venue_owner_addr);
let venue = borrow_global_mut<Venue>(venue_owner_addr);
assert!(current_seat_count < venue.max_seats, EMAX_SEATS);
Vector::push_back(&mut venue.available_tickets, ConcertTicket {seat, ticket_code, price});
}
The first thing you’ll notice is we’ve added a new property to ConcertTicket with price since not all seats will have the same price. So we need to modify our struct to:
struct ConcertTicket has key, store, drop {
seat: vector<u8>,
ticket_code: vector<u8>,
price: u64
}
In the create_ticket function, we first make sure that the Venue has been created, then we grab the number of seats currently created with our available_ticket_count function. We need to do stuff with Venue again, but this time we use borrow_global_mut instead of borrow_global. The only difference here is we are saying we need this value from global storage, and we intend to change something about it. We then check to make sure we’re still under the max_seats property of the Venue with our assert!.
Finally, we can create a ticket and add it to our available_tickets property with the call to Vector::push_back which appends the new ticket to the end of the vector. Note, in the call to push_back we pass a mutable reference to available_tickets so the function call can make changes to it.
Let’s modify our test and add three seats to our arena with this code just after creating and checking the venue:
assert!(exists<Venue>(venue_owner_addr), ENO_VENUE);
// create some tickets
create_ticket(&venue_owner, b"A24", b"AB43C7F", 15);
create_ticket(&venue_owner, b"A25", b"AB43CFD", 15);
create_ticket(&venue_owner, b"A26", b"AB13C7F", 20);
// verify we have 3 tickets now
assert!(available_ticket_count(venue_owner_addr)==3, EINVALID_TICKET_COUNT);
Run the tests with cargo run from terminal and we get all green. All is well; we’ve got three tickets.
Let’s do something just to make a learning point. Rearrange the order of the lines in our create_ticket function so that we set the venue variable before we set current_seat_count:
assert!(exists<Venue>(venue_owner_addr), ENO_VENUE);
let venue = borrow_global_mut<Venue>(venue_owner_addr); let current_seat_count = available_ticket_count(venue_owner_addr);
If you run cargo test again, you’ll get an error:
error[E07003]: invalid operation, could create dangling a reference
┌─ /Users/culbrethw/Development/Tutorials/Tickets/sources/TicketTutorial.move:39:28
│ 38 │ let venue = borrow_global_mut<Venue>(venue_owner_addr);
│ ------------------------------------------ It is still being mutably borrowed by this reference
39 │ let current_seat_count = available_ticket_count(venue_owner_addr);
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Invalid acquiring of resource 'Venue'
What happened? Since we have borrowed <Venue> from global storage, no one else can borrow it until we’re done. Our current_seat_count function tries to borrow that same <Venue> and the safety checks in Move just won’t allow it. Go ahead and change the order back to how we had it.
Can we sell these yet? Almost - but let’s help our potential buyers by letting them find out how much a seat costs. We’ll create another helper function with:
fun get_ticket_info(venue_owner_addr: address, seat:vector<u8>): (bool, vector<u8>, u64, u64) acquires Venue {
let venue = borrow_global<Venue>(venue_owner_addr);
let i = 0;
let len = Vector::length<ConcertTicket>(&venue.available_tickets);
while (i < len) {
let ticket= Vector::borrow<ConcertTicket>(&venue.available_tickets, i);
if (ticket.seat == seat) return (true, ticket.ticket_code, ticket.price, i);
i = i + 1;
};
return (false, b"", 0, 0)
}
The first thing to note - without the explicit public in our function declaration, we’ve made this a private function that can only be called within the module. This allows us to control which part of the ticket info we make available to users with wrapper functions. We’re also returning four different values of bool, vector<u8>, u64, u64 so that we can return success/failure of the function, the ticket_code, the price and the index of where the seat in question sits in the available_tickets. (side note: the code would be more readable in my opinion if we could declare the return with something like ‘success: bool, ticket_code: vector …’ but the Move compiler doesn’t seem to like that - or I haven’t figured out how to do it yet.) To let the user query prices for a particular seat, we’ll create a wrapper function as: public fun get_ticket_price(venue_owner_addr: address, seat:vector<u8>): (bool, u64) acquires Venue {
let (success, _, price, _) = get_ticket_info(venue_owner_addr, seat);assert!(success, EINVALID_TICKET);
return (success, price)
}
One new thing here is the use of the _ notation in place of a variable we’re receiving. The underscore tells the compiler “I know I’ve got to stash this return value somewhere, but I have no plans on using it, so let’s both agree just to trash it.” You can use the underscore by itself, or in front of a variable name like (success, _ticket_code, price, _index) to make the code more readable, but the end result is the same. One related note - because we are discarding values we pulled from a struct, that struct must have the “drop” capability, which you may have noticed we added to ConcertTicket at the top of this episode. Let’s test this functionality now by adding the following to our test code: // verify seat and price
let (success, price) = get_ticket_price(venue_owner_addr, b"A24");
assert!(success, EINVALID_TICKET);
assert!(price==15, EINVALID_PRICE);
We want to know about seat “A24”. The return value in success lets us know that the ticket exists, and then we can make sure the price is the same ‘15’ that we set it to when we created the ticket. We’re almost ready to sell some tickets. We could simply create a function to receive tokens as the purchase price and do a move_to to transfer a ticket to a buyer. Remember, though, we can only have one of any resource type in an account. So if our buyer wants to buy more than one ticket, we need to create a resource to hold multiple tickets. Let’s create a new struct with: struct TicketEnvelope has key {
tickets: vector<ConcertTicket>
}
This gives us a resource we can create for the buyer and hold multiple ConcertTickets. We can (finally) purchase the tickets by adding this function: public fun purchase_ticket(buyer: &signer, venue_owner_addr: address, seat: vector<u8>) acquires Venue, TicketEnvelope {
let buyer_addr = Signer::address_of(buyer);
let (success, _, price, index) = get_ticket_info(venue_owner_addr, seat);
assert!(success, EINVALID_TICKET);
let venue = borrow_global_mut<Venue>(venue_owner_addr);
TestCoin::transfer_internal(buyer, venue_owner_addr, price);
let ticket = Vector::remove<ConcertTicket>(&mut venue.available_tickets, index);
if (!exists<TicketEnvelope>(buyer_addr)) {
move_to<TicketEnvelope>(buyer, TicketEnvelope {tickets: Vector::empty<ConcertTicket>()});
};
let envelope = borrow_global_mut<TicketEnvelope>(buyer_addr);
Vector::push_back<ConcertTicket>(&mut envelope.tickets, ticket);
}
This function lets a
Editor’s Note: I’ve been utilizing the Mirror blogging platform primarily because it signals that I’m a cool crypto guy. However, it’s a bit lacking in functionality. Several folks have expressed an interest in a mechanism to ask questions/post comments on each episode. As such, I’m going to move this blog to Substack (not paid) starting with the next episode. Until then, feel free to hit me up at Twitter or on the Aptos discord. I’ll continue crosspost here on Mirror because I am, in fact, very much a cool crypto guy.
Learning Objectives: Global Storage operators, External Modules, Vectors, Generics, Acquires
We’re picking up where we left off at the last episode - so if this is your first time here check that out first. As always, the repo for the code as it should be at the end of this episode is tagged “Episode-2”.
Last episode we learned how to create a ConcertTicket resource and enable a user to create the ticket and transfer it to their account with:
public fun create_ticket(recipient: &signer, seat: vector<u8>, ticket_code: vector<u8>) {
move_to<ConcertTicket>(recipient, ConcertTicket {seat, ticket_code})
}
While that’s interesting, I’d rather sell tickets to the concert. And we really don’t want the user to be able to assign their seat or everyone’s going to be in the front row. So, let’s jump right in.
We’re going to add several new error constant declarations, so let’s add them just after the structs as:
const ENO_VENUE: u64 = 0;
const ENO_TICKETS: u64 = 1;
const ENO_ENVELOPE: u64 = 2;
const EINVALID_TICKET_COUNT: u64 = 3;
const EINVALID_TICKET: u64 = 4;
const EINVALID_PRICE: u64 = 5;
const EMAX_SEATS: u64 = 6;
const EINVALID_BALANCE: u64 = 7;
The first thing we need to figure out how to do is to limit the actual tickets that can be bought/sold. In the physical world, that’s a fairly easy constraint to comprehend: a venue only has so many physical seats it can sell tickets for. You can’t sell two tickets to the same seat (unless you are United Airlines apparently). Let’s continue to model our resources after their physical world counterpart and create a new struct we’ll call Venue:
struct Venue has key {
available_tickets: vector<ConcertTicket>,
max_seats: u64
}
Add that code to the top of your module just past the ConcertTicket struct. Venue is altogether different from our ConcertTicket struct, with the new twist of a vector of another struct. A venue could be anything from a small club with a handful of seats to a 50,000 seat stadium. Let’s give a venue owner the ability to create a Venue in their Aptos account/wallet:
public fun init_venue(venue_owner: &signer, max_seats: u64) {
let available_tickets = Vector::empty<ConcertTicket>();
move_to<Venue>(venue_owner, Venue {available_tickets, max_seats})
}
This introduces two new concepts that we’ll cover individually: Std::Vector and Generics.
(If you don’t get that subtitle reference, we can’t be friends)
Vector is part of the standard library that comes from the Move language. While vector<T> is a native primitive type, the Vector module adds some wrappers that makes it easier for us to do things with resources. There is a pretty good intro here:
https://diem.github.io/move/vector.html
I overheard my 16 year-old daughter ask a friend of hers “what’s the tea?” while discussing some high school drama - to which I quickly replied, “A generic class or interface that is parameterized over types is typically represented as T in the interface definition.” Apparently, I was incorrect answering “what’s the T” in her context and my helpful answer was not appreciated.
Most of you will be familiar with the <T> notation, but if it’s the first time you’re seeing it, just think of <T> as a parameter for types. In our Move context, we call that a Generic. We sort of glossed over that topic in Episode 1 and there were a few questions.
Since Vector is part of a library, we have to tell the compiler we’re going to use it. In your code just under Std::Signer, let’s add:
use Std::Signer;
use Std::Vector;
In our init_venue function above, we are calling the function Std::Vector::empty<T>() which creates an empty vector<T>. If we peek into the Vector.move module at in the repo at aptos-core/aptos-move/framework/move-stdlib/sources/Vector.move, we can see that Vector::empty is just a function:
// Create an empty vector.
native public fun empty<Element>(): vector<Element>;
Wait, what happened to the <T>? Well, generics are so generic we don’t even specify what you have to call them. Whatever name you put inside the < > in essence becomes a variable that refers to a Type. When a function is defined as:
native public fun empty<Element>(): vector<Element>;
whatever Type we pass in the first element is represented in the rest of the function. So, dropping in <ConcertTicket> for <Element>, the actual function basically becomes:
native public fun empty<ConcertTicket>(): vector<ConcertTicket>;
and anywhere else <Element> appeared in the function would become <ConcertTicket>. That’s all handled behind the scenes for us and we don’t have to worry about it. When defining your own functions with generic type parameters, you can call the parameter T, or Element, or WhateverIDarnWellPlease. It’s just a variable name that holds a type.
So we’ve created a vector of ConcertTicket with
let available_tickets = Vector::empty<ConcertTicket>();
Then we create the Venue resource and move it into our account:
move_to<Venue>(venue_owner, Venue {available_tickets, max_seats})
Now, we could combine those two into one line of code with:
move_to<Venue>(venue_owner, Venue {available_tickets: Vector::empty<ConcertTicket>(), max_seats})
But I think it’s more readable splitting into two lines. Also, to make a very minor point, when setting the property value of a struct, I can use a variable named available_tickets and pass it to Venue {available_tickets} and it will work fine. Because the variable is named the same thing as the parameter, what the compiler sees is Venue {available_tickets: available_tickets}. If you’re passing in anything else, you’ve got to explicitly reference the property name like we did in the one line version above.
Let’s test this and make sure it’s functioning. To start, let’s delete all of our test code from last episode:
#[test(recipient = @0x1)]
public(script) fun sender_can_create_ticket(recipient: signer){
....
}
and replace it with this starting point:
#[test(venue_owner = @0x1)]
public(script) fun sender_can_buy_ticket(venue_owner: signer) {
let venue_owner_addr = Signer::address_of(&venue_owner);
// initialize the venue
init_venue(&venue_owner, 3);
assert!(exists<Venue>(venue_owner_addr), ENO_VENUE);
}
We’ve setup our test to run with an account venue_owner which we are assigning address 0x1 and then we call the init_venue function to create the venue with a maximum of 3 seats. We use our exists function again to make sure the venue was created. From the terminal, run cargo tests and you’ll pass all green.
We can call the init_venue function from our account and we now own an empty arena. Let’s create some tickets to sell. Since we have a maximum number of tickets we can create, it would be helpful to know how many tickets we have already created. Let’s add a helper function of:
public fun available_ticket_count(venue_owner_addr: address): u64 acquires Venue {
let venue = borrow_global<Venue>(venue_owner_addr);
Vector::length<ConcertTicket>(&venue.available_tickets) }
We’ve got several new things going on here. Let’s look at ‘acquires’ first. I’ve got three kids, aged 16, 17 and 21 (yes, I’m old). When each of them first started driving on their own, we had a rule that they always had to tell us where they were going. On occasion, I would use that ever so handy “Find My” iPhone app to see where they were (and they were aware of this). If they ever popped up in a location they hadn’t told me about, I’d give them a call with “I see you are in this location, but you didn’t tell me you were going there. Why did you not want me to know you were going to be there?” It was a temporary practice to ensure their new found freedom wasn’t enabling bad decision making.
In essence, that’s what ‘acquires’ does in the Move language. Move is built for safety. If you intend to access a Venue type from global storage, you’ve got to state that with acquires Venue - otherwise compiler dad is going to call you with The call acquires 'TicketTutorial::Tickets::Venue', but the 'acquires' list for the current function does not contain this type. It must be present in the calling context's acquires list and your code won’t build. Couldn’t the compiler just infer the ‘acquires’ based on my code? Well, sure, but that doesn’t accomplish the safety objective any more than inferring my son is supposed to be at the lake during school hours just because that’s where his phone shows up on the map. It’s a compiler check to make sure what we’re doing in code is what we intended. There is a detailed explanation here:
https://diem.github.io/move/global-storage-operators.html
Now that compiler dad knows where we’re going to be with our ‘acquires’, we can see how many tickets we’ve created. The borrow_global is a global storage operator that allows us to read a particular data type from an account’s global storage. The line borrow_global<Venue>(venue_owner_addr) lets us read the Venue struct belonging to the account owner. We don’t have to specify an index or any other reference because Move will only allow us to create one of any struct at an address. We can either have no Venues or we can have one. That’s it. The ‘borrow’ process ensures data integrity. We can’t borrow a resource if it’s already been borrowed somewhere else. This ensures that we are always getting accurate data. We’ll see this at play shortly.
Key Point: An account can only hold one instance of any resource type.
We’ve got our Venue resource sitting in the aptly named variable venue, now we want to see how many tickets are in the available_tickets property. The function Vector::length gives us that number by passing in the vector we want the count of. Since the vector we want to know about contains ConcertTickets, we pass in the struct as the type parameter and a reference to the available_tickets property of venue. You probably noticed there is no explicit return statement for our function. In the Move language, when the last line of a function returns a value (and we don’t end the line with a semicolon), that value is returned as the value of the function. In essence, the last line of:
Vector::length<ConcertTicket>(&venue.available_tickets)
is actually compiled as if it were:
return Vector::length<ConcertTicket>(&venue.available_tickets);
It’s a handy feature that saves us a tiny bit of time.
We’ve got our helper function now, so let’s create some tickets. We’ll modify our previous create_ticket function from Episode 1 to:
public fun create_ticket(venue_owner: &signer, seat: vector<u8>, ticket_code: vector<u8>, price: u64) acquires Venue {
let venue_owner_addr = Signer::address_of(venue_owner);
assert!(exists<Venue>(venue_owner_addr), ENO_VENUE);
let current_seat_count = available_ticket_count(venue_owner_addr);
let venue = borrow_global_mut<Venue>(venue_owner_addr);
assert!(current_seat_count < venue.max_seats, EMAX_SEATS);
Vector::push_back(&mut venue.available_tickets, ConcertTicket {seat, ticket_code, price});
}
The first thing you’ll notice is we’ve added a new property to ConcertTicket with price since not all seats will have the same price. So we need to modify our struct to:
struct ConcertTicket has key, store, drop {
seat: vector<u8>,
ticket_code: vector<u8>,
price: u64
}
In the create_ticket function, we first make sure that the Venue has been created, then we grab the number of seats currently created with our available_ticket_count function. We need to do stuff with Venue again, but this time we use borrow_global_mut instead of borrow_global. The only difference here is we are saying we need this value from global storage, and we intend to change something about it. We then check to make sure we’re still under the max_seats property of the Venue with our assert!.
Finally, we can create a ticket and add it to our available_tickets property with the call to Vector::push_back which appends the new ticket to the end of the vector. Note, in the call to push_back we pass a mutable reference to available_tickets so the function call can make changes to it.
Let’s modify our test and add three seats to our arena with this code just after creating and checking the venue:
assert!(exists<Venue>(venue_owner_addr), ENO_VENUE);
// create some tickets
create_ticket(&venue_owner, b"A24", b"AB43C7F", 15);
create_ticket(&venue_owner, b"A25", b"AB43CFD", 15);
create_ticket(&venue_owner, b"A26", b"AB13C7F", 20);
// verify we have 3 tickets now
assert!(available_ticket_count(venue_owner_addr)==3, EINVALID_TICKET_COUNT);
Run the tests with cargo run from terminal and we get all green. All is well; we’ve got three tickets.
Let’s do something just to make a learning point. Rearrange the order of the lines in our create_ticket function so that we set the venue variable before we set current_seat_count:
assert!(exists<Venue>(venue_owner_addr), ENO_VENUE);
let venue = borrow_global_mut<Venue>(venue_owner_addr); let current_seat_count = available_ticket_count(venue_owner_addr);
If you run cargo test again, you’ll get an error:
error[E07003]: invalid operation, could create dangling a reference
┌─ /Users/culbrethw/Development/Tutorials/Tickets/sources/TicketTutorial.move:39:28
│ 38 │ let venue = borrow_global_mut<Venue>(venue_owner_addr);
│ ------------------------------------------ It is still being mutably borrowed by this reference
39 │ let current_seat_count = available_ticket_count(venue_owner_addr);
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Invalid acquiring of resource 'Venue'
What happened? Since we have borrowed <Venue> from global storage, no one else can borrow it until we’re done. Our current_seat_count function tries to borrow that same <Venue> and the safety checks in Move just won’t allow it. Go ahead and change the order back to how we had it.
Can we sell these yet? Almost - but let’s help our potential buyers by letting them find out how much a seat costs. We’ll create another helper function with:
fun get_ticket_info(venue_owner_addr: address, seat:vector<u8>): (bool, vector<u8>, u64, u64) acquires Venue {
let venue = borrow_global<Venue>(venue_owner_addr);
let i = 0;
let len = Vector::length<ConcertTicket>(&venue.available_tickets);
while (i < len) {
let ticket= Vector::borrow<ConcertTicket>(&venue.available_tickets, i);
if (ticket.seat == seat) return (true, ticket.ticket_code, ticket.price, i);
i = i + 1;
};
return (false, b"", 0, 0)
}
The first thing to note - without the explicit public in our function declaration, we’ve made this a private function that can only be called within the module. This allows us to control which part of the ticket info we make available to users with wrapper functions. We’re also returning four different values of bool, vector<u8>, u64, u64 so that we can return success/failure of the function, the ticket_code, the price and the index of where the seat in question sits in the available_tickets. (side note: the code would be more readable in my opinion if we could declare the return with something like ‘success: bool, ticket_code: vector …’ but the Move compiler doesn’t seem to like that - or I haven’t figured out how to do it yet.) To let the user query prices for a particular seat, we’ll create a wrapper function as: public fun get_ticket_price(venue_owner_addr: address, seat:vector<u8>): (bool, u64) acquires Venue {
let (success, _, price, _) = get_ticket_info(venue_owner_addr, seat);assert!(success, EINVALID_TICKET);
return (success, price)
}
One new thing here is the use of the _ notation in place of a variable we’re receiving. The underscore tells the compiler “I know I’ve got to stash this return value somewhere, but I have no plans on using it, so let’s both agree just to trash it.” You can use the underscore by itself, or in front of a variable name like (success, _ticket_code, price, _index) to make the code more readable, but the end result is the same. One related note - because we are discarding values we pulled from a struct, that struct must have the “drop” capability, which you may have noticed we added to ConcertTicket at the top of this episode. Let’s test this functionality now by adding the following to our test code: // verify seat and price
let (success, price) = get_ticket_price(venue_owner_addr, b"A24");
assert!(success, EINVALID_TICKET);
assert!(price==15, EINVALID_PRICE);
We want to know about seat “A24”. The return value in success lets us know that the ticket exists, and then we can make sure the price is the same ‘15’ that we set it to when we created the ticket. We’re almost ready to sell some tickets. We could simply create a function to receive tokens as the purchase price and do a move_to to transfer a ticket to a buyer. Remember, though, we can only have one of any resource type in an account. So if our buyer wants to buy more than one ticket, we need to create a resource to hold multiple tickets. Let’s create a new struct with: struct TicketEnvelope has key {
tickets: vector<ConcertTicket>
}
This gives us a resource we can create for the buyer and hold multiple ConcertTickets. We can (finally) purchase the tickets by adding this function: public fun purchase_ticket(buyer: &signer, venue_owner_addr: address, seat: vector<u8>) acquires Venue, TicketEnvelope {
let buyer_addr = Signer::address_of(buyer);
let (success, _, price, index) = get_ticket_info(venue_owner_addr, seat);
assert!(success, EINVALID_TICKET);
let venue = borrow_global_mut<Venue>(venue_owner_addr);
TestCoin::transfer_internal(buyer, venue_owner_addr, price);
let ticket = Vector::remove<ConcertTicket>(&mut venue.available_tickets, index);
if (!exists<TicketEnvelope>(buyer_addr)) {
move_to<TicketEnvelope>(buyer, TicketEnvelope {tickets: Vector::empty<ConcertTicket>()});
};
let envelope = borrow_global_mut<TicketEnvelope>(buyer_addr);
Vector::push_back<ConcertTicket>(&mut envelope.tickets, ticket);
}
This function lets a
buyerget_ticket_infopriceTestCoin::transfer_internalAptosFrameworktransfer_internalbuyervenue_ownerTestCoin::registerTestCoin::BalanceregisterTestCoinregisterbuyerVector::removeindexget_ticket_infoticketavailable_ticketsTicketEnvelopeTicketEnvelopeassert!ticketTicketEnvelopefaucetCoreResources@CoreResourcesvenue_ownerbuyerbuyerbuyerpurchase_ticketcargo testcargo testbuyerget_ticket_infopriceTestCoin::transfer_internalAptosFrameworktransfer_internalbuyervenue_ownerTestCoin::registerTestCoin::BalanceregisterTestCoinregisterbuyerVector::removeindexget_ticket_infoticketavailable_ticketsTicketEnvelopeTicketEnvelopeassert!ticketTicketEnvelopefaucetCoreResources@CoreResourcesvenue_ownerbuyerbuyerbuyerpurchase_ticketcargo testcargo test
No activity yet