github.com/hyperledger/aries-framework-go@v0.3.2/pkg/client/issuecredential/client.go (about)

     1  /*
     2  Copyright SecureKey Technologies Inc. All Rights Reserved.
     3  
     4  SPDX-License-Identifier: Apache-2.0
     5  */
     6  
     7  package issuecredential
     8  
     9  import (
    10  	"errors"
    11  
    12  	"github.com/hyperledger/aries-framework-go/pkg/didcomm/common/service"
    13  	"github.com/hyperledger/aries-framework-go/pkg/didcomm/protocol/decorator"
    14  	"github.com/hyperledger/aries-framework-go/pkg/didcomm/protocol/issuecredential"
    15  	issuecredentialmiddleware "github.com/hyperledger/aries-framework-go/pkg/didcomm/protocol/middleware/issuecredential"
    16  	"github.com/hyperledger/aries-framework-go/pkg/store/connection"
    17  )
    18  
    19  const (
    20  	// web redirect decorator.
    21  	webRedirectDecorator  = "~web-redirect"
    22  	webRedirectStatusFAIL = "FAIL"
    23  )
    24  
    25  var (
    26  	errEmptyOffer    = errors.New("received an empty offer")
    27  	errEmptyProposal = errors.New("received an empty proposal")
    28  	errEmptyRequest  = errors.New("received an empty request")
    29  )
    30  
    31  type (
    32  	// OfferCredential is a message sent by the Issuer to the potential Holder,
    33  	// describing the credential they intend to offer and possibly the price they expect to be paid.
    34  	OfferCredential = issuecredential.OfferCredentialParams
    35  	// OfferCredentialV2 is a message sent by the Issuer to the potential Holder,
    36  	// describing the credential they intend to offer and possibly the price they expect to be paid.
    37  	OfferCredentialV2 issuecredential.OfferCredentialV2
    38  	// OfferCredentialV3 is a message sent by the Issuer to the potential Holder,
    39  	// describing the credential they intend to offer and possibly the price they expect to be paid.
    40  	OfferCredentialV3 issuecredential.OfferCredentialV3
    41  	// ProposeCredential is an optional message sent by the potential Holder to the Issuer
    42  	// to initiate the protocol or in response to a offer-credential message when the Holder
    43  	// wants some adjustments made to the credential data offered by Issuer.
    44  	ProposeCredential = issuecredential.ProposeCredentialParams
    45  	// ProposeCredentialV2 is an optional message sent by the potential Holder to the Issuer
    46  	// to initiate the protocol or in response to a offer-credential message when the Holder
    47  	// wants some adjustments made to the credential data offered by Issuer.
    48  	ProposeCredentialV2 issuecredential.ProposeCredentialV2
    49  	// ProposeCredentialV3 is an optional message sent by the potential Holder to the Issuer
    50  	// to initiate the protocol or in response to a offer-credential message when the Holder
    51  	// wants some adjustments made to the credential data offered by Issuer.
    52  	ProposeCredentialV3 issuecredential.ProposeCredentialV3
    53  	// RequestCredential is a message sent by the potential Holder to the Issuer,
    54  	// to request the issuance of a credential. Where circumstances do not require
    55  	// a preceding Offer Credential message (e.g., there is no cost to issuance
    56  	// that the Issuer needs to explain in advance, and there is no need for cryptographic negotiation),
    57  	// this message initiates the protocol.
    58  	RequestCredential = issuecredential.RequestCredentialParams
    59  	// RequestCredentialV2 is a message sent by the potential Holder to the Issuer,
    60  	// to request the issuance of a credential. Where circumstances do not require
    61  	// a preceding Offer Credential message (e.g., there is no cost to issuance
    62  	// that the Issuer needs to explain in advance, and there is no need for cryptographic negotiation),
    63  	// this message initiates the protocol.
    64  	RequestCredentialV2 issuecredential.RequestCredentialV2
    65  	// RequestCredentialV3 is a message sent by the potential Holder to the Issuer,
    66  	// to request the issuance of a credential. Where circumstances do not require
    67  	// a preceding Offer Credential message (e.g., there is no cost to issuance
    68  	// that the Issuer needs to explain in advance, and there is no need for cryptographic negotiation),
    69  	// this message initiates the protocol.
    70  	RequestCredentialV3 issuecredential.RequestCredentialV3
    71  	// IssueCredential contains as attached payload the credentials being issued and is
    72  	// sent in response to a valid Invitation Credential message.
    73  	IssueCredential = issuecredential.IssueCredentialParams
    74  	// IssueCredentialV2 contains as attached payload the credentials being issued and is
    75  	// sent in response to a valid Invitation Credential message.
    76  	IssueCredentialV2 issuecredential.IssueCredentialV2 //nolint: golint
    77  	// IssueCredentialV3 contains as attached payload the credentials being issued and is
    78  	// sent in response to a valid Invitation Credential message.
    79  	IssueCredentialV3 issuecredential.IssueCredentialV3 //nolint: golint
    80  	// Action contains helpful information about action.
    81  	Action issuecredential.Action
    82  )
    83  
    84  // Provider contains dependencies for the issuecredential protocol and is typically created by using aries.Context().
    85  type Provider interface {
    86  	Service(id string) (interface{}, error)
    87  }
    88  
    89  // ProtocolService defines the issuecredential service.
    90  type ProtocolService interface {
    91  	service.DIDComm
    92  	Actions() ([]issuecredential.Action, error)
    93  	ActionContinue(piID string, opt ...issuecredential.Opt) error
    94  	ActionStop(piID string, err error, opt ...issuecredential.Opt) error
    95  }
    96  
    97  // Client enable access to issuecredential API.
    98  type Client struct {
    99  	service.Event
   100  	service ProtocolService
   101  }
   102  
   103  // New return new instance of the issuecredential client.
   104  func New(ctx Provider) (*Client, error) {
   105  	raw, err := ctx.Service(issuecredential.Name)
   106  	if err != nil {
   107  		return nil, err
   108  	}
   109  
   110  	svc, ok := raw.(ProtocolService)
   111  	if !ok {
   112  		return nil, errors.New("cast service to issuecredential service failed")
   113  	}
   114  
   115  	return &Client{
   116  		Event:   svc,
   117  		service: svc,
   118  	}, nil
   119  }
   120  
   121  // Actions returns unfinished actions for the async usage.
   122  func (c *Client) Actions() ([]Action, error) {
   123  	actions, err := c.service.Actions()
   124  	if err != nil {
   125  		return nil, err
   126  	}
   127  
   128  	result := make([]Action, len(actions))
   129  	for i, action := range actions {
   130  		result[i] = Action(action)
   131  	}
   132  
   133  	return result, nil
   134  }
   135  
   136  // SendOffer is used by the Issuer to send an offer.
   137  func (c *Client) SendOffer(offer *OfferCredential, conn *connection.Record) (string, error) {
   138  	if offer == nil {
   139  		return "", errEmptyOffer
   140  	}
   141  
   142  	var msg service.DIDCommMsg
   143  
   144  	switch conn.DIDCommVersion {
   145  	default:
   146  		fallthrough
   147  	case service.V1:
   148  		offer.Type = issuecredential.OfferCredentialMsgTypeV2
   149  
   150  		msg = service.NewDIDCommMsgMap(offer.AsV2())
   151  	case service.V2:
   152  		offer.Type = issuecredential.OfferCredentialMsgTypeV3
   153  
   154  		msg = service.NewDIDCommMsgMap(offer.AsV3())
   155  	}
   156  
   157  	return c.service.HandleOutbound(msg, conn.MyDID, conn.TheirDID)
   158  }
   159  
   160  // SendProposal is used by the Holder to send a proposal.
   161  func (c *Client) SendProposal(proposal *ProposeCredential, conn *connection.Record) (string, error) {
   162  	if proposal == nil {
   163  		return "", errEmptyProposal
   164  	}
   165  
   166  	var msg service.DIDCommMsg
   167  
   168  	switch conn.DIDCommVersion {
   169  	default:
   170  		fallthrough
   171  	case service.V1:
   172  		proposal.Type = issuecredential.ProposeCredentialMsgTypeV2
   173  
   174  		msg = service.NewDIDCommMsgMap(proposal.AsV2())
   175  	case service.V2:
   176  		proposal.Type = issuecredential.ProposeCredentialMsgTypeV3
   177  
   178  		msg = service.NewDIDCommMsgMap(proposal.AsV3())
   179  	}
   180  
   181  	return c.service.HandleOutbound(msg, conn.MyDID, conn.TheirDID)
   182  }
   183  
   184  // SendRequest is used by the Holder to send a request.
   185  func (c *Client) SendRequest(request *RequestCredential, conn *connection.Record) (string, error) {
   186  	if request == nil {
   187  		return "", errEmptyRequest
   188  	}
   189  
   190  	var msg service.DIDCommMsg
   191  
   192  	switch conn.DIDCommVersion {
   193  	default:
   194  		fallthrough
   195  	case service.V1:
   196  		request.Type = issuecredential.RequestCredentialMsgTypeV2
   197  
   198  		msg = service.NewDIDCommMsgMap(request.AsV2())
   199  	case service.V2:
   200  		request.Type = issuecredential.RequestCredentialMsgTypeV3
   201  
   202  		msg = service.NewDIDCommMsgMap(request.AsV3())
   203  	}
   204  
   205  	return c.service.HandleOutbound(msg, conn.MyDID, conn.TheirDID)
   206  }
   207  
   208  // AcceptProposal is used when the Issuer is willing to accept the proposal.
   209  // NOTE: For async usage.
   210  func (c *Client) AcceptProposal(piID string, msg *OfferCredential) error {
   211  	return c.service.ActionContinue(piID, WithOfferCredential(msg))
   212  }
   213  
   214  // AcceptOffer is used when the Holder is willing to accept the offer.
   215  func (c *Client) AcceptOffer(piID string, msg *RequestCredential) error {
   216  	return c.service.ActionContinue(piID, WithRequestCredential(msg))
   217  }
   218  
   219  // NegotiateProposal is used when the Holder wants to negotiate about an offer he received.
   220  // NOTE: For async usage. This function can be used only after receiving OfferCredential.
   221  func (c *Client) NegotiateProposal(piID string, msg *ProposeCredential) error {
   222  	return c.service.ActionContinue(piID, WithProposeCredential(msg))
   223  }
   224  
   225  // AcceptRequest is used when the Issuer is willing to accept the request.
   226  // NOTE: For async usage.
   227  func (c *Client) AcceptRequest(piID string, msg *IssueCredential) error {
   228  	return c.service.ActionContinue(piID, WithIssueCredential(msg))
   229  }
   230  
   231  // DeclineProposal is used when the Issuer does not want to accept the proposal.
   232  // NOTE: For async usage.
   233  func (c *Client) DeclineProposal(piID, reason string, options ...IssuerDeclineOptions) error {
   234  	return c.service.ActionStop(piID, errors.New(reason), prepareRedirectProperties(webRedirectStatusFAIL, options...))
   235  }
   236  
   237  // DeclineOffer is used when the Holder does not want to accept the offer.
   238  // NOTE: For async usage.
   239  func (c *Client) DeclineOffer(piID, reason string) error {
   240  	return c.service.ActionStop(piID, errors.New(reason))
   241  }
   242  
   243  // DeclineRequest is used when the Issuer does not want to accept the request.
   244  // NOTE: For async usage.
   245  func (c *Client) DeclineRequest(piID, reason string, options ...IssuerDeclineOptions) error {
   246  	return c.service.ActionStop(piID, errors.New(reason), prepareRedirectProperties(webRedirectStatusFAIL, options...))
   247  }
   248  
   249  // AcceptCredential is used when the Holder is willing to accept the IssueCredential.
   250  // NOTE: For async usage.
   251  func (c *Client) AcceptCredential(piID string, options ...AcceptCredentialOptions) error {
   252  	opts := &acceptCredentialOpts{}
   253  
   254  	for _, option := range options {
   255  		option(opts)
   256  	}
   257  
   258  	properties := map[string]interface{}{}
   259  
   260  	if opts.skipStore {
   261  		properties[issuecredentialmiddleware.SkipCredentialSaveKey] = true
   262  	}
   263  
   264  	return c.service.ActionContinue(piID, WithFriendlyNames(opts.names...), issuecredential.WithProperties(properties))
   265  }
   266  
   267  // DeclineCredential is used when the Holder does not want to accept the IssueCredential.
   268  // NOTE: For async usage.
   269  func (c *Client) DeclineCredential(piID, reason string) error {
   270  	return c.service.ActionStop(piID, errors.New(reason))
   271  }
   272  
   273  // AcceptProblemReport accepts problem report action.
   274  func (c *Client) AcceptProblemReport(piID string) error {
   275  	return c.service.ActionContinue(piID)
   276  }
   277  
   278  // WithProposeCredential allows providing ProposeCredential message
   279  // USAGE: This message should be provided after receiving an OfferCredential message.
   280  func WithProposeCredential(msg *ProposeCredential) issuecredential.Opt {
   281  	origin := *msg
   282  
   283  	return issuecredential.WithProposeCredential(&origin)
   284  }
   285  
   286  // WithRequestCredential allows providing RequestCredential message
   287  // USAGE: This message should be provided after receiving an OfferCredential message.
   288  func WithRequestCredential(msg *RequestCredential) issuecredential.Opt {
   289  	origin := *msg
   290  
   291  	return issuecredential.WithRequestCredential(&origin)
   292  }
   293  
   294  // WithOfferCredential allows providing OfferCredential message
   295  // USAGE: This message should be provided after receiving a ProposeCredential message.
   296  func WithOfferCredential(msg *OfferCredential) issuecredential.Opt {
   297  	origin := *msg
   298  
   299  	return issuecredential.WithOfferCredential(&origin)
   300  }
   301  
   302  // WithIssueCredential allows providing IssueCredential message
   303  // USAGE: This message should be provided after receiving a RequestCredential message.
   304  func WithIssueCredential(msg *IssueCredential) issuecredential.Opt {
   305  	origin := *msg
   306  
   307  	return issuecredential.WithIssueCredential(&origin)
   308  }
   309  
   310  // WithFriendlyNames allows providing names for the credentials.
   311  // USAGE: This function should be used when the Holder receives IssueCredential message.
   312  func WithFriendlyNames(names ...string) issuecredential.Opt {
   313  	return issuecredential.WithFriendlyNames(names...)
   314  }
   315  
   316  // acceptCredentialOpts options for accepting credential in holder.
   317  type acceptCredentialOpts struct {
   318  	names     []string
   319  	skipStore bool
   320  }
   321  
   322  // AcceptCredentialOptions is custom option for accepting credential in holder.
   323  type AcceptCredentialOptions func(opts *acceptCredentialOpts)
   324  
   325  // AcceptByFriendlyNames option to provide optional friendly names for accepting credentials.
   326  func AcceptByFriendlyNames(names ...string) AcceptCredentialOptions {
   327  	return func(opts *acceptCredentialOpts) {
   328  		opts.names = names
   329  	}
   330  }
   331  
   332  // AcceptBySkippingStorage skips storing incoming credential to storage.
   333  func AcceptBySkippingStorage() AcceptCredentialOptions {
   334  	return func(opts *acceptCredentialOpts) {
   335  		opts.skipStore = true
   336  	}
   337  }
   338  
   339  // redirectOpts options for web redirect information to holder from issuer.
   340  type redirectOpts struct {
   341  	redirect string
   342  }
   343  
   344  // IssuerDeclineOptions is custom option for sending web redirect options to holder.
   345  // https://github.com/hyperledger/aries-rfcs/tree/main/concepts/0700-oob-through-redirect
   346  type IssuerDeclineOptions func(opts *redirectOpts)
   347  
   348  // RequestRedirect option to provide optional redirect URL requesting holder to redirect.
   349  func RequestRedirect(url string) IssuerDeclineOptions {
   350  	return func(opts *redirectOpts) {
   351  		opts.redirect = url
   352  	}
   353  }
   354  
   355  // create web redirect properties to add ~web-redirect decorator.
   356  func prepareRedirectProperties(status string, options ...IssuerDeclineOptions) issuecredential.Opt {
   357  	properties := map[string]interface{}{}
   358  
   359  	opts := &redirectOpts{}
   360  
   361  	for _, option := range options {
   362  		option(opts)
   363  	}
   364  
   365  	if opts.redirect != "" {
   366  		properties[webRedirectDecorator] = &decorator.WebRedirect{
   367  			Status: status,
   368  			URL:    opts.redirect,
   369  		}
   370  	}
   371  
   372  	return issuecredential.WithProperties(properties)
   373  }